latest commit 06-06-2025
This commit is contained in:
commit
ef79391915
@ -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
|
||||
|
||||
@ -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
@ -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
@ -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
@ -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(),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@ -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
@ -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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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
@ -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
433
lib/Screens/group/groupListBackUp.dart
Normal file
433
lib/Screens/group/groupListBackUp.dart
Normal file
@ -0,0 +1,433 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/Screens/group/group.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import 'groupDetails.dart';
|
||||
|
||||
class GroupListBackUp extends StatefulWidget {
|
||||
@override
|
||||
_GroupListBackUpState createState() => _GroupListBackUpState();
|
||||
}
|
||||
|
||||
class _GroupListBackUpState extends State<GroupListBackUp> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
List<dynamic>? apiAllGroups;
|
||||
Color? layoutColor;
|
||||
Color? bodyColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadAllGroups();
|
||||
loadInitialData();
|
||||
});
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadAllGroups() async {
|
||||
try {
|
||||
final result = await apiService.fetchAllGroup();
|
||||
setState(() {
|
||||
apiAllGroups = result;
|
||||
});
|
||||
print("Fetched services: $apiAllGroups");
|
||||
} catch (e) {
|
||||
print('Error fetching role list: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void handleActiveStatus(
|
||||
Map<String, dynamic> groupData,
|
||||
String groupId,
|
||||
String currentStatus,
|
||||
) async {
|
||||
print("Toggling user status - $groupId (Current: $currentStatus)");
|
||||
|
||||
final String apiUrlData =
|
||||
'$apiUrl/api/groups/update/$groupId'; // API for updating user
|
||||
final String? token = await getToken();
|
||||
|
||||
if (token == null) {
|
||||
print("Error: Token not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1")
|
||||
String newStatus = (currentStatus == "1") ? "0" : "1";
|
||||
|
||||
print("STatus 1 - $newStatus");
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrlData),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
"is_active": newStatus // Set new status dynamically
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
print("User status updated successfully to $newStatus!");
|
||||
loadAllGroups(); // Refresh users list after update
|
||||
} else {
|
||||
print("Failed to update user status. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error updating user status: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void deleteGroup(Map<String, dynamic> groupdata, groupId, status) {
|
||||
print("GroupId : $groupId");
|
||||
print("Groupstatus: $status");
|
||||
print("GroupsData: $groupdata");
|
||||
|
||||
// handleActiveStatus(groupdata, groupId, status);
|
||||
print("Calling handleActiveStatus with: id=$groupId, status=$status");
|
||||
handleActiveStatus(groupdata, groupId.toString(), status.toString());
|
||||
}
|
||||
|
||||
Future<void> refreshData() async {
|
||||
loadAllGroups();
|
||||
}
|
||||
|
||||
// Future<void> deleteGroupFromApi(int groupId) async {
|
||||
// try {
|
||||
// await apiService.deleteGroup(groupId); // your delete API call
|
||||
// deleteGroup(groupId); // remove from UI list
|
||||
// } catch (e) {
|
||||
// print('Error deleting group: $e');
|
||||
// }
|
||||
// }'
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
// backgroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(child: buildGroupListLayout(isDesktop))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildGroupListLayout(bool isDesktop) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
// decoration: BoxDecoration(
|
||||
// // color: Colors.amber,
|
||||
// // color: bodyColor,
|
||||
// color: Color(0xFFE1F5FE),
|
||||
// border: Border.all(
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// width: 3.5)),
|
||||
child: buildGroupData(isDesktop),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget buildGroupListView(bool isDesktop) {
|
||||
// return Container(
|
||||
// child: Text("DAta"),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget buildGroupData(isDesktop) {
|
||||
return Container(
|
||||
// margin: isDesktop
|
||||
// ? EdgeInsets.all(10.0)
|
||||
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
|
||||
// padding: const EdgeInsets.all(10),
|
||||
height: isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
// decoration: BoxDecoration(
|
||||
// border: isDesktop
|
||||
// ? Border.all(
|
||||
// width: 2,
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// )
|
||||
// : null,
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
//
|
||||
// // color: Colors.amber,
|
||||
// ),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Group List',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFF114D8B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// side: BorderSide(color: , width: 1),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: () async {
|
||||
// List<dynamic> users = await futureUsers;
|
||||
// context.go('/CreateGroup');
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => GroupData(
|
||||
isDesktop: isDesktop,
|
||||
groupId: null,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetGroup: refreshData
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text('New Group', style: GoogleFonts.poppins(fontSize: 12)),
|
||||
SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
Icon(
|
||||
Icons.add_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
padding: const EdgeInsets.all(10),
|
||||
// margin: const EdgeInsets.only(bottom: 10),
|
||||
color: Colors.white,
|
||||
// color: Colors.red.shade100,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(
|
||||
children: [
|
||||
buildGroupListView(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupListView(bool isDesktop) {
|
||||
if (apiAllGroups == null || apiAllGroups!.isEmpty) {
|
||||
return Center(child: Text("No groups found."));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: apiAllGroups!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final group = apiAllGroups![index];
|
||||
return Card(
|
||||
// color: bodyColor,
|
||||
// color: Color(0xFFF5F5F5),
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text("Group Name",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("Domestic Policy",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("International Policy",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("Description",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5, fontWeight: FontWeight.w400)),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text("${group['name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("${group['domestic_policy_name'] ?? 'N/A'}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
Expanded(
|
||||
child: Text("${group['international_policy_name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600))),
|
||||
Expanded(
|
||||
child: Text("${group['description'] ?? 'N/A'}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13, fontWeight: FontWeight.w600))),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (group['group_id'] != null) {
|
||||
final newGroupID = int.tryParse(
|
||||
group['group_id'].toString());
|
||||
if (newGroupID != null) {
|
||||
final data = await apiService.getGroupDetailsFind(
|
||||
newGroupID); // ✅ Always an int
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
GroupData(
|
||||
isDesktop: isDesktop,
|
||||
groupId: newGroupID,
|
||||
// Pass the ID
|
||||
groupData: data,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetGroup: refreshData
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
print("something went wrong check properly");
|
||||
}
|
||||
},
|
||||
|
||||
// onTap: () {
|
||||
// context.go("/CreateGroup", extra: group);
|
||||
// },
|
||||
child: Image.asset('assets/images/IconsImg/edit.png',
|
||||
width: 20, height: 15),
|
||||
),
|
||||
SizedBox(
|
||||
width: 5,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final idStr = group['group_id'];
|
||||
final id = int.tryParse(idStr.toString());
|
||||
|
||||
if (id == null) {
|
||||
print("group_id is null");
|
||||
return;
|
||||
}
|
||||
final status = group['is_active'];
|
||||
// print("GroupId : ${group['group_id']} ");
|
||||
deleteGroup(group, id, status);
|
||||
},
|
||||
child: Image.asset('assets/images/IconsImg/delete.png',
|
||||
width: 20, height: 15),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -20,13 +20,14 @@ class HotelsData extends StatefulWidget {
|
||||
final int? hotelsId; // <-- Add this
|
||||
final Map<String, dynamic>? hotelsData;
|
||||
|
||||
const HotelsData(
|
||||
{super.key,
|
||||
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
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
562
lib/Screens/myTemplates/Tempale
Normal file
562
lib/Screens/myTemplates/Tempale
Normal file
@ -0,0 +1,562 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const Template({super.key, required this.templateData});
|
||||
|
||||
static Template fromState(GoRouterState state) {
|
||||
return Template(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateState createState() => TemplateState();
|
||||
}
|
||||
|
||||
class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
// String convertQuillDocToHtml(quill.Document doc) {
|
||||
// final buffer = StringBuffer();
|
||||
//
|
||||
// print("convertQuillDocToHtml");
|
||||
// for (final op in doc.toDelta().toList()) {
|
||||
// final insert = op.data;
|
||||
// final attrs = op.attributes ?? {};
|
||||
//
|
||||
// if (insert is String) {
|
||||
// var content = insert;
|
||||
//
|
||||
// // Handle formatting (bold, italic, etc.)
|
||||
// if (attrs.containsKey('bold')) {
|
||||
// content = '<strong>$content</strong>';
|
||||
// }
|
||||
// if (attrs.containsKey('italic')) {
|
||||
// content = '<em>$content</em>';
|
||||
// }
|
||||
//
|
||||
// // Wrap each paragraph with <p>
|
||||
// if (content.trim().isNotEmpty) {
|
||||
// buffer.write('<p>${content.trim()}</p>');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String convertQuillDocToHtml2(quill.Document doc) {
|
||||
// final buffer = StringBuffer();
|
||||
// final lines = <String>[];
|
||||
// final delta = doc.toDelta();
|
||||
//
|
||||
// String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
// if (attrs == null) return text;
|
||||
// if (attrs.containsKey('bold')) {
|
||||
// text = '<strong>$text</strong>';
|
||||
// }
|
||||
// if (attrs.containsKey('italic')) {
|
||||
// text = '<em>$text</em>';
|
||||
// }
|
||||
// return text;
|
||||
// }
|
||||
//
|
||||
// for (final op in delta.toList()) {
|
||||
// final insert = op.data;
|
||||
// final attrs = op.attributes;
|
||||
//
|
||||
// if (insert is String) {
|
||||
// final parts = insert.split('\n');
|
||||
// for (int i = 0; i < parts.length; i++) {
|
||||
// final part = applyStyles(parts[i], attrs);
|
||||
// lines.add(part);
|
||||
//
|
||||
// if (i < parts.length - 1) {
|
||||
// // End of line: wrap accumulated content into <p>
|
||||
// final joined = lines.join('');
|
||||
// if (joined.trim().isNotEmpty) {
|
||||
// buffer.writeln('<p>${joined.trim()}</p>');
|
||||
// }
|
||||
// lines.clear();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Add remaining lines
|
||||
// final joined = lines.join('');
|
||||
// if (joined.trim().isNotEmpty) {
|
||||
// buffer.writeln('<p>${joined.trim()}</p>');
|
||||
// }
|
||||
//
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String extractPlainTextFromHtml(String html) {
|
||||
// final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
// final matches = regex.allMatches(html);
|
||||
//
|
||||
// final buffer = StringBuffer();
|
||||
// for (final match in matches) {
|
||||
// final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
// buffer.writeln(text.trim());
|
||||
// }
|
||||
// return buffer.toString();
|
||||
// }
|
||||
//
|
||||
// String decodeHtmlEntities(String text) {
|
||||
// return text
|
||||
// .replaceAll(' ', ' ')
|
||||
// .replaceAll('&', '&')
|
||||
// .replaceAll('<', '<')
|
||||
// .replaceAll('>', '>')
|
||||
// .replaceAll('"', '"')
|
||||
// .replaceAll(''', "'"); // add more as needed
|
||||
// }
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
// /*inal converter = DeltaFromHTML();
|
||||
// final delta = converter.convert(bodyHtml); // Convert HTML → Delta
|
||||
// final quillDoc = quill.Document.fromDelta(delta);
|
||||
// */
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
//
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// // final quillDoc = convertBasicHtmlToQuill(bodyHtml);
|
||||
// // final quillDoc = quill.Document()..insert(0, plainText);
|
||||
// // final quillDoc = quill.Document.fromDelta(delta);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
final deltaJsonString =
|
||||
widget.templateData?["templateData"]?["body_delta"];
|
||||
if (deltaJsonString != null) {
|
||||
final deltaJson = jsonDecode(deltaJsonString);
|
||||
final quillDoc = quill.Document.fromJson(deltaJson);
|
||||
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(offset: 0),
|
||||
);
|
||||
}
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
setState(() {
|
||||
// updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
|
||||
final TemplateData1 = TemplateData;
|
||||
print("TemplateData - $TemplateData1");
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(Map<String, dynamic> policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop,
|
||||
context,
|
||||
bodyColor,
|
||||
layoutColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop,
|
||||
context,
|
||||
Color? bodyColor,
|
||||
Color layoutColor,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(height: 10),
|
||||
buildTempalteSubject(isDesktop),
|
||||
|
||||
SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(child: QuillSimpleToolbar(controller: _controller)),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/templateList');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
const kScreenshot1 = 'assets/images/screenshot_1.png';
|
||||
const kScreenshot2 = 'assets/images/screenshot_2.png';
|
||||
const kScreenshot3 = 'assets/images/screenshot_3.png';
|
||||
const kScreenshot4 = 'assets/images/screenshot_4.png';
|
||||
const kScreenshot4 =
|
||||
'assets/images/screenshot_4.png'; // TODO Implement this library.
|
||||
|
||||
@ -16,14 +16,8 @@ class CustomToolbar extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(
|
||||
children: [
|
||||
QuillToolbarHistoryButton(
|
||||
isUndo: true,
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarHistoryButton(
|
||||
isUndo: false,
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarHistoryButton(isUndo: true, controller: controller),
|
||||
QuillToolbarHistoryButton(isUndo: false, controller: controller),
|
||||
QuillToolbarToggleStyleButton(
|
||||
options: const QuillToolbarToggleStyleButtonOptions(),
|
||||
controller: controller,
|
||||
@ -38,40 +32,22 @@ class CustomToolbar extends StatelessWidget {
|
||||
controller: controller,
|
||||
attribute: Attribute.underline,
|
||||
),
|
||||
QuillToolbarClearFormatButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarClearFormatButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarImageButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarCameraButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarVideoButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarImageButton(controller: controller),
|
||||
QuillToolbarCameraButton(controller: controller),
|
||||
QuillToolbarVideoButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarColorButton(
|
||||
controller: controller,
|
||||
isBackground: false,
|
||||
),
|
||||
QuillToolbarColorButton(
|
||||
controller: controller,
|
||||
isBackground: true,
|
||||
),
|
||||
QuillToolbarColorButton(controller: controller, isBackground: false),
|
||||
QuillToolbarColorButton(controller: controller, isBackground: true),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarSelectHeaderStyleDropdownButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarSelectHeaderStyleDropdownButton(controller: controller),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarSelectLineHeightStyleDropdownButton(
|
||||
controller: controller,
|
||||
),
|
||||
const VerticalDivider(),
|
||||
QuillToolbarToggleCheckListButton(
|
||||
controller: controller,
|
||||
),
|
||||
QuillToolbarToggleCheckListButton(controller: controller),
|
||||
QuillToolbarToggleStyleButton(
|
||||
controller: controller,
|
||||
attribute: Attribute.ol,
|
||||
@ -88,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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
lib/Screens/myTemplates/dialog_placeholders.dart
Normal file
88
lib/Screens/myTemplates/dialog_placeholders.dart
Normal file
@ -0,0 +1,88 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:frontend/utils/auth_utils.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class PlaceholdersModal extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> placeholders;
|
||||
const PlaceholdersModal({Key? key, required this.placeholders})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
_PlaceholdersModalState createState() => _PlaceholdersModalState();
|
||||
}
|
||||
|
||||
class _PlaceholdersModalState extends State<PlaceholdersModal> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String templLabel(String placeholder) {
|
||||
var label = placeholder.replaceAll('%', '').replaceAll('_', ' ');
|
||||
return label
|
||||
.split(' ')
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? word[0].toUpperCase() + word.substring(1)
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
title: Text(
|
||||
"Available Placeholders",
|
||||
style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
content: Container(
|
||||
width:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.width * 0.4
|
||||
: double.maxFinite,
|
||||
// Set max height so ListView knows constraints
|
||||
height: 300,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.placeholders.length,
|
||||
itemBuilder: (context, index) {
|
||||
final value = widget.placeholders[index]['value'] ?? '';
|
||||
return ListTile(
|
||||
hoverColor: Colors.white,
|
||||
focusColor: Colors.white,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
templLabel(value),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SelectableText(
|
||||
value,
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
// onTap: () {
|
||||
// Navigator.of(context).pop(value);
|
||||
// },
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text("Close", style: GoogleFonts.poppins(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../editor/image/image_embed_types.dart';
|
||||
import 'extensions/controller_ext.dart';
|
||||
|
||||
OnImageInsertCallback _defaultOnImageInsert() {
|
||||
return (imageUrl, controller) async {
|
||||
controller
|
||||
..skipRequestKeyboard = true
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
..insertImageBlock(imageSource: imageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
@internal
|
||||
Future<void> handleImageInsert(
|
||||
String imageUrl, {
|
||||
required QuillController controller,
|
||||
required OnImageInsertCallback? onImageInsertCallback,
|
||||
required OnImageInsertedCallback? onImageInsertedCallback,
|
||||
}) async {
|
||||
final customOnImageInsert = onImageInsertCallback;
|
||||
if (customOnImageInsert != null) {
|
||||
await customOnImageInsert.call(imageUrl, controller);
|
||||
} else {
|
||||
await _defaultOnImageInsert().call(imageUrl, controller);
|
||||
}
|
||||
await onImageInsertedCallback?.call(imageUrl);
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../toolbar/video/config/video.dart';
|
||||
import 'extensions/controller_ext.dart';
|
||||
|
||||
OnVideoInsertCallback _defaultOnVideoInsert() {
|
||||
return (imageUrl, controller) async {
|
||||
controller
|
||||
..skipRequestKeyboard = true
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
..insertVideoBlock(videoUrl: imageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
@internal
|
||||
Future<void> handleVideoInsert(
|
||||
String videoUrl, {
|
||||
required QuillController controller,
|
||||
required OnVideoInsertCallback? onVideoInsertCallback,
|
||||
required OnVideoInsertedCallback? onVideoInsertedCallback,
|
||||
}) async {
|
||||
final customOnVideoInsert = onVideoInsertCallback;
|
||||
if (customOnVideoInsert != null) {
|
||||
await customOnVideoInsert.call(videoUrl, controller);
|
||||
} else {
|
||||
await _defaultOnVideoInsert().call(videoUrl, controller);
|
||||
}
|
||||
await onVideoInsertedCallback?.call(videoUrl);
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart'
|
||||
show Attribute, AttributeScope;
|
||||
|
||||
class FlutterAlignmentAttribute extends Attribute<String?> {
|
||||
const FlutterAlignmentAttribute(String? val)
|
||||
: super('flutterAlignment', AttributeScope.ignore, val);
|
||||
}
|
||||
|
||||
extension AttributeExt on Attribute {
|
||||
static const FlutterAlignmentAttribute flutterAlignment =
|
||||
FlutterAlignmentAttribute(null);
|
||||
}
|
||||
@ -1,36 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
@Deprecated('Invalid extension')
|
||||
extension QuillControllerExt on QuillController {
|
||||
@Deprecated(
|
||||
'Invalid extension property and will be removed, use selection.baseOffset instead')
|
||||
int get index => selection.baseOffset;
|
||||
@Deprecated(
|
||||
'Invalid extension property and will be removed, use selection.extentOffset - selection.baseOffset instead')
|
||||
int get length => selection.extentOffset - index;
|
||||
|
||||
@Deprecated('Invalid extension method and will be removed.')
|
||||
void insertImageBlock({
|
||||
required String imageSource,
|
||||
}) {
|
||||
this
|
||||
..skipRequestKeyboard = true
|
||||
..replaceText(
|
||||
index,
|
||||
length,
|
||||
BlockEmbed.image(imageSource),
|
||||
null,
|
||||
)
|
||||
..moveCursorToPosition(index + 1);
|
||||
}
|
||||
|
||||
@Deprecated('Invalid extension method and will be removed.')
|
||||
void insertVideoBlock({
|
||||
required String videoUrl,
|
||||
}) {
|
||||
this
|
||||
..skipRequestKeyboard = true
|
||||
..replaceText(index, length, BlockEmbed.video(videoUrl), null)
|
||||
..moveCursorToPosition(index + 1);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' show QuillDialogTheme;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'utils/patterns.dart';
|
||||
|
||||
enum LinkType {
|
||||
video,
|
||||
image,
|
||||
}
|
||||
|
||||
class TypeLinkDialog extends StatefulWidget {
|
||||
const TypeLinkDialog({
|
||||
required this.linkType,
|
||||
this.dialogTheme,
|
||||
this.link,
|
||||
this.linkRegExp,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final QuillDialogTheme? dialogTheme;
|
||||
final String? link;
|
||||
final RegExp? linkRegExp;
|
||||
final LinkType linkType;
|
||||
|
||||
@override
|
||||
TypeLinkDialogState createState() => TypeLinkDialogState();
|
||||
}
|
||||
|
||||
class TypeLinkDialogState extends State<TypeLinkDialog> {
|
||||
late String _link;
|
||||
late TextEditingController _controller;
|
||||
RegExp? _linkRegExp;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_link = widget.link ?? '';
|
||||
_controller = TextEditingController(text: _link);
|
||||
|
||||
_linkRegExp = widget.linkRegExp;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
|
||||
content: TextField(
|
||||
keyboardType: TextInputType.url,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: null,
|
||||
style: widget.dialogTheme?.inputTextStyle,
|
||||
decoration: InputDecoration(
|
||||
labelText: context.loc.pasteLink,
|
||||
hintText: widget.linkType == LinkType.image
|
||||
? context.loc.pleaseEnterAValidImageURL
|
||||
: context.loc.pleaseEnterAValidVideoURL,
|
||||
labelStyle: widget.dialogTheme?.labelTextStyle,
|
||||
floatingLabelStyle: widget.dialogTheme?.labelTextStyle,
|
||||
),
|
||||
autofocus: true,
|
||||
onChanged: _linkChanged,
|
||||
controller: _controller,
|
||||
onEditingComplete: () {
|
||||
if (!_canPress()) {
|
||||
return;
|
||||
}
|
||||
_applyLink();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _canPress() ? _applyLink : null,
|
||||
child: Text(
|
||||
context.loc.ok,
|
||||
style: widget.dialogTheme?.labelTextStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _linkChanged(String value) {
|
||||
setState(() {
|
||||
_link = value;
|
||||
});
|
||||
}
|
||||
|
||||
void _applyLink() {
|
||||
Navigator.pop(context, _link.trim());
|
||||
}
|
||||
|
||||
RegExp get linkRegExp {
|
||||
final customRegExp = _linkRegExp;
|
||||
if (customRegExp != null) {
|
||||
return customRegExp;
|
||||
}
|
||||
switch (widget.linkType) {
|
||||
case LinkType.video:
|
||||
if (youtubeRegExp.hasMatch(_link)) {
|
||||
return youtubeRegExp;
|
||||
}
|
||||
return videoRegExp;
|
||||
case LinkType.image:
|
||||
return imageRegExp;
|
||||
}
|
||||
}
|
||||
|
||||
bool _canPress() {
|
||||
if (_link.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (widget.linkType == LinkType.image) {}
|
||||
return _link.isNotEmpty && linkRegExp.hasMatch(_link);
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
// import 'package:universal_html/html.dart' as html;
|
||||
|
||||
// Fake interface for the logic that this package needs from (web-only) dart:ui.
|
||||
// This is conditionally exported so the analyzer sees these methods as
|
||||
// available.
|
||||
|
||||
// typedef PlatroformViewFactory = html.Element Function(int viewId);
|
||||
|
||||
// /// Shim for web_ui engine.PlatformViewRegistry
|
||||
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L62
|
||||
// class PlatformViewRegistry {
|
||||
// /// Shim for registerViewFactory
|
||||
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/ui.dart#L72
|
||||
// static dynamic registerViewFactory(
|
||||
// String viewTypeId, PlatroformViewFactory viewFactory) {}
|
||||
// }
|
||||
|
||||
// /// Shim for web_ui engine.AssetManager
|
||||
// /// https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/assets.dart#L12
|
||||
// class WebOnlyAssetManager {
|
||||
// static dynamic getAssetUrl(String asset) {}
|
||||
// }
|
||||
|
||||
class PlatformViewRegistry {
|
||||
/// Register [viewType] as being created by the given [viewFactory].
|
||||
///
|
||||
/// [viewFactory] can be any function that takes an integer and optional
|
||||
/// `params` and returns an `HTMLElement` DOM object.
|
||||
bool registerViewFactory(
|
||||
String viewType,
|
||||
Function viewFactory, {
|
||||
bool isVisible = true,
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns the view previously created for [viewId].
|
||||
///
|
||||
/// Throws if no view has been created for [viewId].
|
||||
Object getViewById(int viewId) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export 'dart:ui' if (dart.library.js_interop) 'dart:ui_web';
|
||||
@ -1,84 +0,0 @@
|
||||
import 'package:flutter/widgets.dart' show BuildContext, MediaQuery;
|
||||
|
||||
Map<String, String> parseCssString(String cssString) {
|
||||
final result = <String, String>{};
|
||||
final declarations = cssString.split(';');
|
||||
|
||||
for (final declaration in declarations) {
|
||||
final parts = declaration.split(':');
|
||||
if (parts.length == 2) {
|
||||
final property = parts[0].trim();
|
||||
final value = parts[1].trim();
|
||||
result[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
enum _CssUnit {
|
||||
px('px'),
|
||||
percentage('%'),
|
||||
viewportWidth('vw'),
|
||||
viewportHeight('vh'),
|
||||
em('em'),
|
||||
rem('rem'),
|
||||
invalid('invalid');
|
||||
|
||||
const _CssUnit(this.cssName);
|
||||
|
||||
final String cssName;
|
||||
}
|
||||
|
||||
double? parseCssPropertyAsDouble(
|
||||
String value, {
|
||||
required BuildContext context,
|
||||
}) {
|
||||
if (value.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to parse it in case it's a valid double already
|
||||
var doubleValue = double.tryParse(value);
|
||||
|
||||
if (doubleValue != null) {
|
||||
return doubleValue;
|
||||
}
|
||||
|
||||
// If not then if it's a css numberic value then we will try to parse it
|
||||
final unit = _CssUnit.values
|
||||
.where((element) => value.endsWith(element.cssName))
|
||||
.firstOrNull;
|
||||
if (unit == null) {
|
||||
return null;
|
||||
}
|
||||
value = value.replaceFirst(unit.cssName, '');
|
||||
doubleValue = double.tryParse(value);
|
||||
if (doubleValue != null) {
|
||||
switch (unit) {
|
||||
case _CssUnit.px:
|
||||
// Do nothing
|
||||
break;
|
||||
case _CssUnit.percentage:
|
||||
// Not supported yet
|
||||
doubleValue = null;
|
||||
break;
|
||||
case _CssUnit.viewportWidth:
|
||||
doubleValue = (doubleValue / 100) * MediaQuery.sizeOf(context).width;
|
||||
break;
|
||||
case _CssUnit.viewportHeight:
|
||||
doubleValue = (doubleValue / 100) * MediaQuery.sizeOf(context).height;
|
||||
break;
|
||||
case _CssUnit.em:
|
||||
doubleValue = MediaQuery.textScalerOf(context).scale(doubleValue);
|
||||
break;
|
||||
case _CssUnit.rem:
|
||||
doubleValue = MediaQuery.textScalerOf(context).scale(doubleValue);
|
||||
break;
|
||||
case _CssUnit.invalid:
|
||||
doubleValue = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return doubleValue;
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
import 'package:flutter/foundation.dart' show immutable;
|
||||
import 'package:flutter/widgets.dart' show Alignment, BuildContext;
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'element_shared_utils.dart';
|
||||
|
||||
/// Theses properties are not officialy supported by quill js
|
||||
/// but they are only used in all platforms other than web
|
||||
/// and they will be stored in css style property so quill js ignore them
|
||||
enum ExtraElementProperties {
|
||||
deletable,
|
||||
}
|
||||
|
||||
(
|
||||
ElementSize elementSize,
|
||||
double? margin,
|
||||
Alignment alignment,
|
||||
) getElementAttributes(
|
||||
Node node,
|
||||
BuildContext context,
|
||||
) {
|
||||
var elementSize = const ElementSize(null, null);
|
||||
var elementAlignment = Alignment.center;
|
||||
double? elementMargin;
|
||||
|
||||
final heightValue = parseCssPropertyAsDouble(
|
||||
node.style.attributes[Attribute.height.key]?.value.toString() ?? '',
|
||||
context: context,
|
||||
);
|
||||
final widthValue = parseCssPropertyAsDouble(
|
||||
node.style.attributes[Attribute.width.key]?.value.toString() ?? '',
|
||||
context: context,
|
||||
);
|
||||
|
||||
if (heightValue != null) {
|
||||
elementSize = elementSize.copyWith(
|
||||
height: heightValue,
|
||||
);
|
||||
}
|
||||
if (widthValue != null) {
|
||||
elementSize = elementSize.copyWith(
|
||||
width: widthValue,
|
||||
);
|
||||
}
|
||||
|
||||
final cssStyle = node.style.attributes['style'];
|
||||
|
||||
if (cssStyle != null) {
|
||||
// It css value as string but we will try to support it anyway
|
||||
|
||||
final cssAttrs = parseCssString(cssStyle.value.toString());
|
||||
|
||||
final cssHeightValue = parseCssPropertyAsDouble(
|
||||
(cssAttrs[Attribute.height.key]) ?? '',
|
||||
context: context,
|
||||
);
|
||||
final cssWidthValue = parseCssPropertyAsDouble(
|
||||
(cssAttrs[Attribute.width.key]) ?? '',
|
||||
context: context,
|
||||
);
|
||||
|
||||
// cssHeightValue != null && elementSize.height == null
|
||||
if (cssHeightValue != null) {
|
||||
elementSize = elementSize.copyWith(height: cssHeightValue);
|
||||
}
|
||||
if (cssWidthValue != null) {
|
||||
elementSize = elementSize.copyWith(width: cssWidthValue);
|
||||
}
|
||||
|
||||
elementAlignment = getAlignment(cssAttrs['alignment']);
|
||||
|
||||
final margin = double.tryParse('margin');
|
||||
if (margin != null) {
|
||||
elementMargin = margin;
|
||||
}
|
||||
}
|
||||
|
||||
return (elementSize, elementMargin, elementAlignment);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ElementSize {
|
||||
const ElementSize(
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
|
||||
/// If non-null, requires the child to have exactly this width.
|
||||
/// If null, the child is free to choose its own width.
|
||||
final double? width;
|
||||
|
||||
/// If non-null, requires the child to have exactly this height.
|
||||
/// If null, the child is free to choose its own height.
|
||||
final double? height;
|
||||
|
||||
ElementSize copyWith({
|
||||
double? width,
|
||||
double? height,
|
||||
}) {
|
||||
return ElementSize(
|
||||
width ?? this.width,
|
||||
height ?? this.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Attribute, Node;
|
||||
|
||||
import 'element_shared_utils.dart';
|
||||
|
||||
/// Prefer the width, and height from the css style attribute if exits
|
||||
/// it can be `auto` or `100px` so it's specific to HTML && CSS
|
||||
/// if not, we will use the one from attributes which is usually just an double
|
||||
(
|
||||
String height,
|
||||
String width,
|
||||
String margin,
|
||||
String alignment,
|
||||
) getWebElementAttributes(
|
||||
Node node,
|
||||
) {
|
||||
var height = 'auto';
|
||||
var width = 'auto';
|
||||
// TODO(): Add support for margin and alignment
|
||||
var margin = 'auto';
|
||||
const alignment = 'center';
|
||||
|
||||
final cssStyle = node.style.attributes['style'];
|
||||
|
||||
final heightValue = node.style.attributes[Attribute.height.key]?.value;
|
||||
final widthValue = node.style.attributes[Attribute.width.key]?.value;
|
||||
|
||||
if (cssStyle != null) {
|
||||
final attrs = parseCssString(cssStyle.value.toString());
|
||||
|
||||
final cssHeightValue = attrs[Attribute.height.key];
|
||||
|
||||
if (cssHeightValue != null) {
|
||||
height = cssHeightValue;
|
||||
} else {
|
||||
height = '${heightValue}px';
|
||||
}
|
||||
final cssWidthValue = attrs[Attribute.width.key];
|
||||
if (cssWidthValue != null) {
|
||||
width = cssWidthValue;
|
||||
} else if (widthValue != null) {
|
||||
width = '${widthValue}px';
|
||||
}
|
||||
|
||||
final cssMarginValue = attrs['margin'];
|
||||
if (cssMarginValue != null) {
|
||||
margin = cssMarginValue;
|
||||
}
|
||||
|
||||
return (height, width, margin, alignment);
|
||||
}
|
||||
|
||||
if (heightValue != null) {
|
||||
height = '${heightValue}px';
|
||||
}
|
||||
if (widthValue != null) {
|
||||
width = '${widthValue}px';
|
||||
}
|
||||
|
||||
return (height, width, margin, alignment);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
RegExp base64RegExp = RegExp(
|
||||
r'^(?:[A-Za-z0-9+\/][A-Za-z0-9+\/][A-Za-z0-9+\/][A-Za-z0-9+\/])*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$',
|
||||
);
|
||||
|
||||
final imageRegExp = RegExp(
|
||||
r'https?://.*?\.(?:png|jpe?g|gif|bmp|webp|tiff?)',
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final videoRegExp = RegExp(
|
||||
r'\bhttps?://\S+\.(mp4|mov|avi|mkv|flv|wmv|webm)\b',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final youtubeRegExp = RegExp(
|
||||
r'^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
@ -1,30 +0,0 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart' show Attribute;
|
||||
|
||||
String replaceStyleStringWithSize(
|
||||
String cssStyle, {
|
||||
required double width,
|
||||
required double height,
|
||||
}) {
|
||||
final result = <String, String>{};
|
||||
final pairs = cssStyle.split(';');
|
||||
for (final pair in pairs) {
|
||||
final index = pair.indexOf(':');
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
final key = pair.substring(0, index).trim();
|
||||
result[key] = pair.substring(index + 1).trim();
|
||||
}
|
||||
|
||||
result[Attribute.width.key] = width.toString();
|
||||
result[Attribute.height.key] = height.toString();
|
||||
final sb = StringBuffer();
|
||||
for (final pair in result.entries) {
|
||||
sb
|
||||
..write(pair.key)
|
||||
..write(': ')
|
||||
..write(pair.value)
|
||||
..write('; ');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
import 'patterns.dart';
|
||||
|
||||
bool isBase64(String str) {
|
||||
return base64RegExp.hasMatch(str);
|
||||
}
|
||||
|
||||
bool isHttpUrl(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url.trim());
|
||||
return uri.isScheme('HTTP') || uri.isScheme('HTTPS');
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isImageBase64(String imageUrl) {
|
||||
return !isHttpUrl(imageUrl) && isBase64(imageUrl);
|
||||
}
|
||||
|
||||
bool isYouTubeUrl(String videoUrl) {
|
||||
try {
|
||||
final uri = Uri.parse(videoUrl);
|
||||
return uri.host == 'www.youtube.com' ||
|
||||
uri.host == 'youtube.com' ||
|
||||
uri.host == 'youtu.be' ||
|
||||
uri.host == 'www.youtu.be';
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export './web_stub.dart' if (dart.library.js_interop) './web_real.dart';
|
||||
@ -1,46 +0,0 @@
|
||||
import 'package:web/web.dart';
|
||||
import '../dart_ui/dart_ui_fake.dart'
|
||||
if (dart.library.js_interop) '../dart_ui/dart_ui_real.dart' as ui;
|
||||
|
||||
void main(List<String> args) {
|
||||
HTMLImageElement;
|
||||
}
|
||||
|
||||
void createHtmlImageElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) {
|
||||
ui.PlatformViewRegistry().registerViewFactory(src, (viewId) {
|
||||
return createHtmlImageElement(
|
||||
src: src,
|
||||
alignSelf: alignSelf,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void createHtmlIFrameElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) {
|
||||
ui.PlatformViewRegistry().registerViewFactory(
|
||||
src,
|
||||
(id) {
|
||||
return HTMLIFrameElement()
|
||||
..style.width = width
|
||||
..style.height = height
|
||||
..src = src
|
||||
..style.border = 'none'
|
||||
..style.margin = margin
|
||||
..style.alignSelf = alignSelf;
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
void createHtmlImageElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) =>
|
||||
throw UnimplementedError(
|
||||
'A stub method is called, createHtmlImageElement is for web platforms only.');
|
||||
|
||||
void createHtmlIFrameElement({
|
||||
required String src,
|
||||
required String height,
|
||||
required String width,
|
||||
required String margin,
|
||||
required String alignSelf,
|
||||
}) =>
|
||||
throw UnimplementedError(
|
||||
'A stub method is called, createHtmlIFrameElement is for web platforms only.');
|
||||
@ -1,165 +1 @@
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import '../image_embed_types.dart';
|
||||
|
||||
/// [QuillEditorImageEmbedConfig] for desktop, mobile and
|
||||
/// other platforms
|
||||
/// excluding web, it's configurations that is needed for the editor
|
||||
///
|
||||
@immutable
|
||||
class QuillEditorImageEmbedConfig {
|
||||
const QuillEditorImageEmbedConfig({
|
||||
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
|
||||
this.shouldRemoveImageCallback,
|
||||
this.imageProviderBuilder,
|
||||
this.imageErrorWidgetBuilder,
|
||||
this.onImageClicked,
|
||||
}) : _onImageRemovedCallback = onImageRemovedCallback;
|
||||
|
||||
/// [onImageRemovedCallback] is called when an image is
|
||||
/// removed from the editor.
|
||||
/// By default, [onImageRemovedCallback] deletes the
|
||||
/// temporary image file if
|
||||
/// the platform is mobile and if it still exists. You
|
||||
/// can customize this behavior
|
||||
/// by passing your own function that handles the removal process.
|
||||
///
|
||||
/// Example of [onImageRemovedCallback] customization:
|
||||
/// ```dart
|
||||
/// afterRemoveImageFromEditor: (imageFile) async {
|
||||
/// // Your custom logic here
|
||||
/// // or leave it empty to do nothing
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Default value if the passed value is null:
|
||||
/// [QuillEditorImageEmbedConfig.defaultOnImageRemovedCallback]
|
||||
///
|
||||
/// so if you want to do nothing make sure to pass a empty callback
|
||||
/// instead of passing null as value
|
||||
final ImageEmbedBuilderOnRemovedCallback? _onImageRemovedCallback;
|
||||
|
||||
ImageEmbedBuilderOnRemovedCallback get onImageRemovedCallback {
|
||||
return _onImageRemovedCallback ??
|
||||
QuillEditorImageEmbedConfig.defaultOnImageRemovedCallback;
|
||||
}
|
||||
|
||||
/// [shouldRemoveImageCallback] is a callback
|
||||
/// function that is invoked when the
|
||||
/// user attempts to remove an image from the editor. It allows you to control
|
||||
/// whether the image should be removed based on your custom logic.
|
||||
///
|
||||
/// Example of [shouldRemoveImageCallback] customization:
|
||||
/// ```dart
|
||||
/// shouldRemoveImageFromEditor: (imageFile) async {
|
||||
/// // Show a confirmation dialog before removing the image
|
||||
/// final isShouldRemove = await showYesCancelDialog(
|
||||
/// context: context,
|
||||
/// options: const YesOrCancelDialogOptions(
|
||||
/// title: 'Deleting an image',
|
||||
/// message: 'Are you sure you want' ' to delete this
|
||||
/// image from the editor?',
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// // Return `true` to allow image removal if the user confirms, otherwise
|
||||
/// `false`
|
||||
/// return isShouldRemove;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
final ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback;
|
||||
|
||||
/// Allows to override the default handling and fallback to the default if `null` was returned.
|
||||
///
|
||||
/// Example of [imageProviderBuilder] customization:
|
||||
/// ```dart
|
||||
/// imageProviderBuilder: (imageUrl) async {
|
||||
/// if (imageUrl.startsWith('assets/')) {
|
||||
/// // Supports Image assets
|
||||
/// return AssetImage(imageUrl);
|
||||
/// }
|
||||
/// if (imageUrl.startsWith('http')) {
|
||||
/// // Use https://pub.dev/packages/cached_network_image
|
||||
/// // for network images to cache them.
|
||||
/// return CachedNetworkImageProvider(imageUrl);
|
||||
/// }
|
||||
///
|
||||
/// // Return null to fallback to default handling
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
final ImageEmbedBuilderProviderBuilder? imageProviderBuilder;
|
||||
|
||||
/// [imageErrorWidgetBuilder] if you want to show a custom widget based on the
|
||||
/// exception that happen while loading the image, if it network image or
|
||||
/// local one, and it will get called on all the images even in the photo
|
||||
/// preview widget and not just in the quill editor
|
||||
/// by default the default error from flutter framework will thrown
|
||||
///
|
||||
final ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder;
|
||||
|
||||
/// What should happen when the image is pressed?
|
||||
///
|
||||
/// By default will show `ImageOptionsMenu` dialog. If you want to handle what happens
|
||||
/// to the image when it's clicked, you can pass a callback to this property.
|
||||
final void Function(String imageSource)? onImageClicked;
|
||||
|
||||
static ImageEmbedBuilderOnRemovedCallback get defaultOnImageRemovedCallback {
|
||||
return (imageUrl) async {
|
||||
if (kIsWeb) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mobile = isMobileApp;
|
||||
// If the platform is not mobile, return void;
|
||||
// Since the mobile OS gives us a copy of the image
|
||||
|
||||
// Note: We should remove the image on Flutter web
|
||||
// since the behavior is similar to how it is on mobile,
|
||||
// but since this builder is not for web, we will ignore it
|
||||
if (!mobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On mobile OS (Android, iOS), the system will not give us
|
||||
// direct access to the image; instead,
|
||||
// it will give us the image
|
||||
// in the temp directory of the application. So, we want to
|
||||
// remove it when we no longer need it.
|
||||
|
||||
// but on desktop we don't want to touch user files
|
||||
// especially on macOS, where we can't even delete
|
||||
// it without
|
||||
// permission
|
||||
|
||||
final dartIoImageFile = File(imageUrl);
|
||||
|
||||
final isFileExists = await dartIoImageFile.exists();
|
||||
if (isFileExists) {
|
||||
await dartIoImageFile.delete();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
QuillEditorImageEmbedConfig copyWith({
|
||||
ImageEmbedBuilderOnRemovedCallback? onImageRemovedCallback,
|
||||
ImageEmbedBuilderWillRemoveCallback? shouldRemoveImageCallback,
|
||||
ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||
ImageEmbedBuilderErrorWidgetBuilder? imageErrorWidgetBuilder,
|
||||
bool? forceUseMobileOptionMenuForImageClick,
|
||||
}) {
|
||||
return QuillEditorImageEmbedConfig(
|
||||
onImageRemovedCallback: onImageRemovedCallback ?? _onImageRemovedCallback,
|
||||
shouldRemoveImageCallback:
|
||||
shouldRemoveImageCallback ?? this.shouldRemoveImageCallback,
|
||||
imageProviderBuilder: imageProviderBuilder ?? this.imageProviderBuilder,
|
||||
imageErrorWidgetBuilder:
|
||||
imageErrorWidgetBuilder ?? this.imageErrorWidgetBuilder,
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,11 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BoxConstraints;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorWebImageEmbedConfig {
|
||||
const QuillEditorWebImageEmbedConfig({
|
||||
this.constraints,
|
||||
});
|
||||
|
||||
final BoxConstraints? constraints;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,77 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_utils.dart';
|
||||
import 'config/image_config.dart';
|
||||
import 'image_menu.dart';
|
||||
import 'widgets/image.dart';
|
||||
|
||||
class QuillEditorImageEmbedBuilder extends EmbedBuilder {
|
||||
QuillEditorImageEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
final QuillEditorImageEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.imageType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
final imageSource = standardizeImageUrl(embedContext.node.value.data);
|
||||
final ((imageSize), margin, alignment) = getElementAttributes(
|
||||
embedContext.node,
|
||||
context,
|
||||
);
|
||||
|
||||
final width = imageSize.width;
|
||||
final height = imageSize.height;
|
||||
|
||||
final imageWidget = getImageWidgetByImageSource(
|
||||
context: context,
|
||||
imageSource,
|
||||
imageProviderBuilder: config.imageProviderBuilder,
|
||||
imageErrorWidgetBuilder: config.imageErrorWidgetBuilder,
|
||||
alignment: alignment,
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final onImageClicked = config.onImageClicked;
|
||||
if (onImageClicked != null) {
|
||||
onImageClicked(imageSource);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => ImageOptionsMenu(
|
||||
controller: embedContext.controller,
|
||||
config: config,
|
||||
imageSource: imageSource,
|
||||
imageSize: imageSize,
|
||||
readOnly: embedContext.readOnly,
|
||||
imageProvider: imageWidget.image,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (margin != null) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(margin),
|
||||
child: imageWidget,
|
||||
);
|
||||
}
|
||||
return imageWidget;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,67 +1 @@
|
||||
import 'package:flutter/widgets.dart'
|
||||
show ImageErrorWidgetBuilder, ImageProvider;
|
||||
import 'package:flutter/widgets.dart' show BuildContext;
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
/// When request picking an image, for example when the image button toolbar
|
||||
/// clicked, it should be null in case the user didn't choose any image or
|
||||
/// any other reasons, and it should be the image file path as string that is
|
||||
/// exists in case the user picked the image successfully
|
||||
///
|
||||
/// by default we already have a default implementation that show a dialog
|
||||
/// request the source for picking the image, from gallery, link or camera
|
||||
typedef OnRequestPickImage = Future<String?> Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
/// A callback will called when inserting a image in the editor
|
||||
/// it have the logic that will insert the image block using the controller
|
||||
typedef OnImageInsertCallback = Future<void> Function(
|
||||
String image,
|
||||
QuillController controller,
|
||||
);
|
||||
|
||||
/// When a new image picked this callback will called and you might want to
|
||||
/// do some logic depending on your use case
|
||||
typedef OnImageInsertedCallback = Future<void> Function(
|
||||
String image,
|
||||
);
|
||||
|
||||
enum InsertImageSource {
|
||||
gallery,
|
||||
camera,
|
||||
link,
|
||||
}
|
||||
|
||||
/// Configurations for dealing with images, on insert a image
|
||||
/// on request picking a image
|
||||
@immutable
|
||||
class QuillToolbarImageConfig {
|
||||
const QuillToolbarImageConfig({
|
||||
this.onRequestPickImage,
|
||||
this.onImageInsertedCallback,
|
||||
this.onImageInsertCallback,
|
||||
});
|
||||
|
||||
final OnRequestPickImage? onRequestPickImage;
|
||||
|
||||
final OnImageInsertedCallback? onImageInsertedCallback;
|
||||
|
||||
final OnImageInsertCallback? onImageInsertCallback;
|
||||
}
|
||||
|
||||
typedef ImageEmbedBuilderWillRemoveCallback = Future<bool> Function(
|
||||
String imageUrl,
|
||||
);
|
||||
|
||||
typedef ImageEmbedBuilderOnRemovedCallback = Future<void> Function(
|
||||
String imageUrl,
|
||||
);
|
||||
|
||||
typedef ImageEmbedBuilderProviderBuilder = ImageProvider? Function(
|
||||
BuildContext context,
|
||||
String imageUrl,
|
||||
);
|
||||
|
||||
typedef ImageEmbedBuilderErrorWidgetBuilder = ImageErrorWidgetBuilder;
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import 'dart:async' show Completer;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ImageLoader {
|
||||
static ImageLoader _instance = ImageLoader();
|
||||
|
||||
static ImageLoader get instance => _instance;
|
||||
|
||||
/// Allows overriding the instance for testing
|
||||
@visibleForTesting
|
||||
static set instance(ImageLoader newInstance) => _instance = newInstance;
|
||||
|
||||
// TODO(performance): This will load the image again. In case
|
||||
// this is a network image, then this will be inefficient.
|
||||
Future<Uint8List?> loadImageBytesFromImageProvider({
|
||||
required ImageProvider imageProvider,
|
||||
}) async {
|
||||
final stream = imageProvider.resolve(ImageConfiguration.empty);
|
||||
final completer = Completer<ui.Image>();
|
||||
|
||||
ImageStreamListener? listener;
|
||||
listener = ImageStreamListener((info, _) {
|
||||
completer.complete(info.image);
|
||||
stream.removeListener(listener!);
|
||||
});
|
||||
|
||||
stream.addListener(listener);
|
||||
|
||||
final image = await completer.future;
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
return byteData?.buffer.asUint8List();
|
||||
}
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart' show showCupertinoModalPopup;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart'
|
||||
show ImageUrl, QuillController, StyleAttribute, getEmbedNode;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_utils.dart';
|
||||
import '../../common/utils/string.dart';
|
||||
import 'config/image_config.dart';
|
||||
import 'image_load_utils.dart';
|
||||
import 'image_save_utils.dart';
|
||||
import 'widgets/image.dart' show ImageTapWrapper, getImageStyleString;
|
||||
import 'widgets/image_resizer.dart' show ImageResizer;
|
||||
|
||||
class ImageOptionsMenu extends StatelessWidget {
|
||||
const ImageOptionsMenu({
|
||||
required this.controller,
|
||||
required this.config,
|
||||
required this.imageSource,
|
||||
required this.imageSize,
|
||||
required this.readOnly,
|
||||
required this.imageProvider,
|
||||
this.prefersGallerySave = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final QuillController controller;
|
||||
final QuillEditorImageEmbedConfig config;
|
||||
final String imageSource;
|
||||
final ElementSize imageSize;
|
||||
final bool readOnly;
|
||||
final ImageProvider imageProvider;
|
||||
|
||||
// TODO(quill_native_bridge): Update this doc comment once saveImageToGallery()
|
||||
// is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features
|
||||
/// Determines if the image should be saved to the gallery instead of using the
|
||||
/// system file save dialog for platforms that support both.
|
||||
///
|
||||
/// Currently, the only platform where this applies is macOS.
|
||||
///
|
||||
/// This is silently ignored on platforms that only support gallery save (Android and iOS)
|
||||
/// or only image save.
|
||||
///
|
||||
/// For more details, refer to [quill_native_bridge Saving images](https://pub.dev/packages/quill_native_bridge#-saving-images).
|
||||
final bool prefersGallerySave;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final materialTheme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
|
||||
child: SimpleDialog(
|
||||
title: Text(context.loc.image),
|
||||
children: [
|
||||
if (!readOnly)
|
||||
ListTile(
|
||||
title: Text(context.loc.resize),
|
||||
leading: const Icon(Icons.settings_outlined),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (modalContext) {
|
||||
final screenSize = MediaQuery.sizeOf(modalContext);
|
||||
return ImageResizer(
|
||||
onImageResize: (width, height) {
|
||||
final res = getEmbedNode(
|
||||
controller,
|
||||
controller.selection.start,
|
||||
);
|
||||
|
||||
final attr = replaceStyleStringWithSize(
|
||||
getImageStyleString(controller),
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
controller
|
||||
..skipRequestKeyboard = true
|
||||
..formatText(
|
||||
res.offset,
|
||||
1,
|
||||
StyleAttribute(attr),
|
||||
);
|
||||
},
|
||||
imageWidth: imageSize.width,
|
||||
imageHeight: imageSize.height,
|
||||
maxWidth: screenSize.width,
|
||||
maxHeight: screenSize.height,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy_all_outlined),
|
||||
title: Text(context.loc.copy),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
controller.copiedImageUrl = ImageUrl(
|
||||
imageSource,
|
||||
getImageStyleString(controller),
|
||||
);
|
||||
|
||||
final imageBytes = await ImageLoader.instance
|
||||
.loadImageBytesFromImageProvider(
|
||||
imageProvider: imageProvider);
|
||||
if (imageBytes != null) {
|
||||
await ClipboardServiceProvider.instance.copyImage(imageBytes);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!readOnly)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.delete_forever_outlined,
|
||||
color: materialTheme.colorScheme.error,
|
||||
),
|
||||
title: Text(context.loc.remove),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Call the remove check callback if set
|
||||
if (await config.shouldRemoveImageCallback?.call(imageSource) ==
|
||||
false) {
|
||||
return;
|
||||
}
|
||||
|
||||
final offset = getEmbedNode(
|
||||
controller,
|
||||
controller.selection.start,
|
||||
).offset;
|
||||
controller.replaceText(
|
||||
offset,
|
||||
1,
|
||||
'',
|
||||
TextSelection.collapsed(offset: offset),
|
||||
);
|
||||
// Call the post remove callback if set
|
||||
await config.onImageRemovedCallback.call(imageSource);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text(context.loc.save),
|
||||
onTap: () async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final localizations = context.loc;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
SaveImageResult? result;
|
||||
try {
|
||||
result = await ImageSaver.instance.saveImage(
|
||||
imageUrl: imageSource,
|
||||
imageProvider: imageProvider,
|
||||
prefersGallerySave: prefersGallerySave,
|
||||
);
|
||||
} on GalleryImageSaveAccessDeniedException {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
localizations.saveImagePermissionDenied,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(
|
||||
localizations.errorUnexpectedSavingImage,
|
||||
)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(localizations.successImageDownloaded)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isGallerySave) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(localizations.successImageSavedGallery),
|
||||
action: SnackBarAction(
|
||||
label: localizations.openGallery,
|
||||
onPressed: () =>
|
||||
QuillNativeProvider.instance.openGalleryApp(),
|
||||
),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopApp) {
|
||||
final imageFilePath = result.imageFilePath;
|
||||
if (imageFilePath == null) {
|
||||
// User canceled the system save dialog.
|
||||
return;
|
||||
}
|
||||
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(localizations.successImageSaved),
|
||||
// On macOS the app only has access to the picked file from the system save
|
||||
// dialog and not the directory where it was saved.
|
||||
// Opening the directory of that file requires entitlements on macOS
|
||||
// See https://pub.dev/packages/url_launcher#macos-file-access-configuration
|
||||
// Open the saved image file instead of the directory
|
||||
action: defaultTargetPlatform == TargetPlatform.macOS
|
||||
? SnackBarAction(
|
||||
label: localizations.openFile,
|
||||
onPressed: () => launchUrl(Uri.file(imageFilePath)),
|
||||
)
|
||||
: SnackBarAction(
|
||||
label: localizations.openFileLocation,
|
||||
onPressed: () => launchUrl(
|
||||
Uri.directory(p.dirname(imageFilePath))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw StateError(
|
||||
'Image save result is not handled on $defaultTargetPlatform');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.zoom_in),
|
||||
title: Text(context.loc.zoom),
|
||||
onTap: () => Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ImageTapWrapper(
|
||||
imageUrl: imageSource,
|
||||
config: config,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,254 +0,0 @@
|
||||
@internal
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'image_load_utils.dart';
|
||||
|
||||
const defaultImageFileExtension = 'png';
|
||||
|
||||
// The [imageSourcePath] could be file, asset path or HTTP image URL.
|
||||
String extractImageFileExtensionFromImageSource(String? imageSourcePath) {
|
||||
if (imageSourcePath == null || imageSourcePath.isEmpty) {
|
||||
return defaultImageFileExtension;
|
||||
}
|
||||
|
||||
if (!imageSourcePath.contains('.')) {
|
||||
return defaultImageFileExtension;
|
||||
}
|
||||
|
||||
return p.extension(imageSourcePath).replaceFirst('.', '');
|
||||
}
|
||||
|
||||
// The [imageSourcePath] could be file, asset path or HTTP image URL.
|
||||
String? extractImageNameFromImageSource(String? imageSourcePath) {
|
||||
if (imageSourcePath == null || imageSourcePath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final uri = Uri.parse(imageSourcePath);
|
||||
final pathWithoutQuery = uri.path;
|
||||
|
||||
final imageName = p.basenameWithoutExtension(pathWithoutQuery);
|
||||
if (imageName.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return imageName;
|
||||
}
|
||||
|
||||
class SaveImageResult {
|
||||
const SaveImageResult({
|
||||
required this.imageFilePath,
|
||||
required this.isGallerySave,
|
||||
});
|
||||
|
||||
/// Returns `null` on web platforms, if [isGallerySave] is `true`
|
||||
/// or in case the user cancels the save operation on desktop platforms.
|
||||
final String? imageFilePath;
|
||||
|
||||
final bool isGallerySave;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(other, this)) return true;
|
||||
if (other is! SaveImageResult) return false;
|
||||
return other.imageFilePath == imageFilePath &&
|
||||
other.isGallerySave == isGallerySave;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(imageFilePath, isGallerySave);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SaveImageResult(imageFilePath: $imageFilePath, isGallerySave: $isGallerySave)';
|
||||
}
|
||||
|
||||
const String defaultImageFileNamePrefix = 'IMG';
|
||||
|
||||
String getDefaultImageFileName({required bool isGallerySave}) {
|
||||
if (kIsWeb) {
|
||||
// The browser handles name conflicts.
|
||||
return defaultImageFileNamePrefix;
|
||||
}
|
||||
if (isGallerySave) {
|
||||
// The gallery app handles name conflicts.
|
||||
return defaultImageFileNamePrefix;
|
||||
}
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS ||
|
||||
defaultTargetPlatform == TargetPlatform.windows) {
|
||||
// Windows and macOS system native save dialog prompts the user to confirm file overwrite.
|
||||
return defaultImageFileNamePrefix;
|
||||
}
|
||||
final uniqueFileName =
|
||||
'${defaultImageFileNamePrefix}_${DateTime.now().toIso8601String()}';
|
||||
if (defaultTargetPlatform == TargetPlatform.linux) {
|
||||
// IMPORTANT: On Linux, it depends on the desktop environment
|
||||
// and name conflicts may not be handled. Always provide a unique image file name.
|
||||
return uniqueFileName;
|
||||
}
|
||||
|
||||
return uniqueFileName;
|
||||
}
|
||||
|
||||
Future<bool> shouldSaveToGallery({required bool prefersGallerySave}) async {
|
||||
final supportsGallerySave = await QuillNativeProvider.instance
|
||||
.isSupported(QuillNativeBridgeFeature.saveImageToGallery);
|
||||
if (!supportsGallerySave) {
|
||||
return false;
|
||||
}
|
||||
final supportsImageSave = await QuillNativeProvider.instance
|
||||
.isSupported(QuillNativeBridgeFeature.saveImage);
|
||||
if (!supportsImageSave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return supportsGallerySave && prefersGallerySave;
|
||||
}
|
||||
|
||||
/// Thrown when the gallery image save operation is denied
|
||||
/// due to insufficient or denied permissions.
|
||||
class GalleryImageSaveAccessDeniedException implements Exception {
|
||||
GalleryImageSaveAccessDeniedException([this.message]);
|
||||
|
||||
final String? message;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
message ??
|
||||
'Permission to save the image to the gallery was denied or insufficient.';
|
||||
}
|
||||
|
||||
class ImageSaver {
|
||||
ImageSaver._();
|
||||
|
||||
static ImageSaver _instance = ImageSaver._();
|
||||
|
||||
static ImageSaver get instance => _instance;
|
||||
|
||||
/// Allows overriding the instance for testing
|
||||
@visibleForTesting
|
||||
static set instance(ImageSaver newInstance) => _instance = newInstance;
|
||||
|
||||
/// Saves an image to the user's device based on the platform:
|
||||
///
|
||||
/// - **Web**: Downloads the image using the browser's download functionality.
|
||||
/// - **Desktop**: Prompts the user to choose a location for the image using
|
||||
/// native save dialog, defaulting to the user's `Pictures` directory. Or
|
||||
/// saves the image to the gallery in case [prefersGallerySave] is `true` and
|
||||
// TODO(quill_native_bridge): Update this doc comment once saveImageToGallery()
|
||||
// is supported on Windows too (will be applicable like macOS). See https://pub.dev/packages/quill_native_bridge#-features
|
||||
/// the gallery is supported (currently only macOS is applicable).
|
||||
/// - **Mobile**: Saves the image to the gallery, requesting permission if needed.
|
||||
///
|
||||
/// The [imageUrl] could be file or network image URL and is used to extract
|
||||
/// image file extension and the image name.
|
||||
///
|
||||
/// The [imageProvider] is used to load the image bytes from using [ImageLoader].
|
||||
///
|
||||
/// Returns `null` on failure.
|
||||
///
|
||||
/// Throws [GalleryImageSaveAccessDeniedException] in case permission was denied or insuffeicnet.
|
||||
Future<SaveImageResult?> saveImage({
|
||||
required String imageUrl,
|
||||
required ImageProvider imageProvider,
|
||||
required bool prefersGallerySave,
|
||||
}) async {
|
||||
assert(() {
|
||||
if (imageUrl.isEmpty) {
|
||||
throw ArgumentError.value(imageUrl, 'imageUrl', 'cannot be empty');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
final imageFileExtension =
|
||||
extractImageFileExtensionFromImageSource(imageUrl);
|
||||
final imageName = extractImageNameFromImageSource(imageUrl);
|
||||
|
||||
final imageBytes = await ImageLoader.instance
|
||||
.loadImageBytesFromImageProvider(imageProvider: imageProvider);
|
||||
if (imageBytes == null || imageBytes.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
await QuillNativeProvider.instance.saveImage(
|
||||
imageBytes,
|
||||
options: ImageSaveOptions(
|
||||
name: imageName ?? getDefaultImageFileName(isGallerySave: false),
|
||||
fileExtension: imageFileExtension),
|
||||
);
|
||||
return const SaveImageResult(
|
||||
imageFilePath: null,
|
||||
isGallerySave: false,
|
||||
);
|
||||
}
|
||||
|
||||
if (await shouldSaveToGallery(prefersGallerySave: prefersGallerySave)) {
|
||||
try {
|
||||
await QuillNativeProvider.instance.saveImageToGallery(
|
||||
imageBytes,
|
||||
options: GalleryImageSaveOptions(
|
||||
name: imageName ?? getDefaultImageFileName(isGallerySave: true),
|
||||
fileExtension: imageFileExtension,
|
||||
// Specifying the album name requires read-write permission
|
||||
// on iOS and macOS on all versions. Pass null to request add-only on
|
||||
// supported versions (previous versions still use read-write).
|
||||
albumName: null,
|
||||
),
|
||||
);
|
||||
|
||||
return const SaveImageResult(
|
||||
imageFilePath: null,
|
||||
isGallerySave: true,
|
||||
);
|
||||
} on PlatformException catch (e) {
|
||||
// TODO(save-image): Part of https://github.com/FlutterQuill/quill-native-bridge/issues/2
|
||||
|
||||
// Permission request is required only on iOS, macOS and Android API 28 and earlier.
|
||||
if (e.code == 'PERMISSION_DENIED') {
|
||||
// macOS imposes security restrictions when running the app
|
||||
// on sources other than Xcode or the macOS terminal, such as Android Studio or VS Code.
|
||||
// This is not an issue in production. Throwing [GalleryImageSaveAccessDeniedException] will indicate
|
||||
// that the user denied the permission, even though it will always deny the permission even if granted.
|
||||
// Make sure we don't handle that error (it has details) during development to avoid confusion.
|
||||
// For more details, see https://github.com/flutter/flutter/issues/134191#issuecomment-2506248266
|
||||
// and https://pub.dev/packages/quill_native_bridge#-saving-images-to-the-gallery
|
||||
|
||||
final possiblePermissionIssueDuringDevelopmentOnMacOS =
|
||||
kDebugMode && defaultTargetPlatform == TargetPlatform.macOS;
|
||||
if (possiblePermissionIssueDuringDevelopmentOnMacOS) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
throw GalleryImageSaveAccessDeniedException(e.toString());
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
if (await QuillNativeProvider.instance
|
||||
.isSupported(QuillNativeBridgeFeature.saveImage)) {
|
||||
assert(!isMobileApp,
|
||||
'Mobile platforms support saving images to the gallery only');
|
||||
|
||||
final result = await QuillNativeProvider.instance.saveImage(
|
||||
imageBytes,
|
||||
options: ImageSaveOptions(
|
||||
name: imageName ?? getDefaultImageFileName(isGallerySave: false),
|
||||
fileExtension: imageFileExtension,
|
||||
),
|
||||
);
|
||||
return SaveImageResult(
|
||||
imageFilePath: result.filePath,
|
||||
isGallerySave: false,
|
||||
);
|
||||
}
|
||||
|
||||
throw StateError('Image save is not handled on $defaultTargetPlatform');
|
||||
}
|
||||
}
|
||||
@ -1,64 +1 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_web_utils.dart';
|
||||
import '../../common/utils/utils.dart';
|
||||
import '../../common/utils/web/web.dart';
|
||||
import 'config/image_web_config.dart';
|
||||
|
||||
class QuillEditorWebImageEmbedBuilder extends EmbedBuilder {
|
||||
const QuillEditorWebImageEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
|
||||
final QuillEditorWebImageEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.imageType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
assert(kIsWeb, 'ImageEmbedBuilderWeb is only for web platform');
|
||||
|
||||
final (height, width, margin, alignment) =
|
||||
getWebElementAttributes(embedContext.node);
|
||||
|
||||
var imageSource = embedContext.node.value.data.toString();
|
||||
|
||||
// This logic make sure if the image is imageBase64 then
|
||||
// it make sure if the pattern is like
|
||||
// data:image/png;base64, [base64 encoded image string here]
|
||||
// if not then it will add the data:image/png;base64, at the first
|
||||
if (isImageBase64(imageSource)) {
|
||||
// Sometimes the image base 64 for some reasons
|
||||
// doesn't displayed with the 'data:image/png;base64'
|
||||
if (!(imageSource.startsWith('data:image/') &&
|
||||
imageSource.contains('base64'))) {
|
||||
imageSource = 'data:image/png;base64, $imageSource';
|
||||
}
|
||||
}
|
||||
|
||||
createHtmlImageElement(
|
||||
src: imageSource,
|
||||
alignSelf: alignment,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints:
|
||||
config.constraints ?? BoxConstraints.loose(const Size(200, 200)),
|
||||
child: HtmlElementView(
|
||||
viewType: imageSource,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
import 'dart:convert' show base64;
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
import '../../../common/utils/utils.dart';
|
||||
import '../config/image_config.dart';
|
||||
import '../image_embed_types.dart';
|
||||
|
||||
String getImageStyleString(QuillController controller) {
|
||||
final String? s = controller
|
||||
.getAllSelectionStyles()
|
||||
.firstWhere((s) => s.attributes.containsKey(Attribute.style.key),
|
||||
orElse: Style.new)
|
||||
.attributes[Attribute.style.key]
|
||||
?.value;
|
||||
return s ?? '';
|
||||
}
|
||||
|
||||
/// [imageProviderBuilder] To override the return value pass value to it
|
||||
/// [imageSource] The source of the image in the quill delta json document
|
||||
/// It could be http, file, network, asset, or base 64 image
|
||||
ImageProvider getImageProviderByImageSource(
|
||||
String imageSource, {
|
||||
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
if (imageProviderBuilder != null) {
|
||||
final imageProvider = imageProviderBuilder(context, imageSource);
|
||||
if (imageProvider != null) {
|
||||
return imageProvider;
|
||||
}
|
||||
}
|
||||
|
||||
if (isImageBase64(imageSource)) {
|
||||
return MemoryImage(base64.decode(imageSource));
|
||||
}
|
||||
|
||||
if (isHttpUrl(imageSource)) {
|
||||
return NetworkImage(imageSource);
|
||||
}
|
||||
|
||||
// File image
|
||||
if (kIsWeb) {
|
||||
return NetworkImage(imageSource);
|
||||
}
|
||||
return FileImage(File(imageSource));
|
||||
}
|
||||
|
||||
Image getImageWidgetByImageSource(
|
||||
String imageSource, {
|
||||
required BuildContext context,
|
||||
required ImageEmbedBuilderProviderBuilder? imageProviderBuilder,
|
||||
required ImageErrorWidgetBuilder? imageErrorWidgetBuilder,
|
||||
double? width,
|
||||
double? height,
|
||||
AlignmentGeometry alignment = Alignment.center,
|
||||
}) {
|
||||
return Image(
|
||||
image: getImageProviderByImageSource(
|
||||
context: context,
|
||||
imageSource,
|
||||
imageProviderBuilder: imageProviderBuilder,
|
||||
),
|
||||
width: width,
|
||||
height: height,
|
||||
alignment: alignment,
|
||||
errorBuilder: imageErrorWidgetBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
String standardizeImageUrl(String url) {
|
||||
if (url.contains('base64')) {
|
||||
return url.split(',')[1];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
const List<String> _imageFileExtensions = [
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.jpg',
|
||||
'.gif',
|
||||
'.webp',
|
||||
'.tif',
|
||||
'.heic'
|
||||
];
|
||||
|
||||
/// This is a bug of Gallery Saver Package.
|
||||
/// It can not save image that's filename does not end with it's file extension
|
||||
/// like below.
|
||||
// "https://firebasestorage.googleapis.com/v0/b/eventat-4ba96.appspot.com/o/2019-Metrology-Events.jpg?alt=media&token=bfc47032-5173-4b3f-86bb-9659f46b362a"
|
||||
/// If imageUrl does not end with it's file extension,
|
||||
/// file extension is added to image url for saving.
|
||||
String appendFileExtensionToImageUrl(String url) {
|
||||
final endsWithImageFileExtension = _imageFileExtensions
|
||||
.firstWhere((s) => url.toLowerCase().endsWith(s), orElse: () => '');
|
||||
if (endsWithImageFileExtension.isNotEmpty) {
|
||||
return url;
|
||||
}
|
||||
|
||||
final imageFileExtension = _imageFileExtensions
|
||||
.firstWhere((s) => url.toLowerCase().contains(s), orElse: () => '');
|
||||
|
||||
return url + imageFileExtension;
|
||||
}
|
||||
|
||||
class ImageTapWrapper extends StatelessWidget {
|
||||
const ImageTapWrapper({
|
||||
required this.imageUrl,
|
||||
required this.config,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String imageUrl;
|
||||
final QuillEditorImageEmbedConfig config;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
constraints: BoxConstraints.expand(
|
||||
height: MediaQuery.sizeOf(context).height,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
PhotoView(
|
||||
imageProvider: getImageProviderByImageSource(
|
||||
context: context,
|
||||
imageUrl,
|
||||
imageProviderBuilder: config.imageProviderBuilder,
|
||||
),
|
||||
errorBuilder: config.imageErrorWidgetBuilder,
|
||||
loadingBuilder: (context, event) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
right: 10,
|
||||
top: MediaQuery.paddingOf(context).top + 10.0,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.2,
|
||||
child: Container(
|
||||
height: 30,
|
||||
width: 30,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: Colors.grey[400],
|
||||
size: 28,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart'
|
||||
show CupertinoActionSheet, CupertinoActionSheetAction;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart' show SchedulerBinding;
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
class ImageResizer extends StatefulWidget {
|
||||
const ImageResizer({
|
||||
required this.imageWidth,
|
||||
required this.imageHeight,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
required this.onImageResize,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final double? imageWidth;
|
||||
final double? imageHeight;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
final Function(double width, double height) onImageResize;
|
||||
|
||||
@override
|
||||
ImageResizerState createState() => ImageResizerState();
|
||||
}
|
||||
|
||||
class ImageResizerState extends State<ImageResizer> {
|
||||
late double _width;
|
||||
late double _height;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_width = widget.imageWidth ?? widget.maxWidth;
|
||||
_height = widget.imageHeight ?? widget.maxHeight;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Theme.of(context).isCupertino) {
|
||||
return _showCupertinoMenu();
|
||||
}
|
||||
return _showMaterialMenu();
|
||||
}
|
||||
|
||||
Widget _showMaterialMenu() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_widthSlider(),
|
||||
_heightSlider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _showCupertinoMenu() {
|
||||
return CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {},
|
||||
child: _widthSlider(),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () {},
|
||||
child: _heightSlider(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _slider({
|
||||
required bool isWidth,
|
||||
required ValueChanged<double> onChanged,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Card(
|
||||
child: Slider.adaptive(
|
||||
value: isWidth ? _width : _height,
|
||||
max: isWidth ? widget.maxWidth : widget.maxHeight,
|
||||
divisions: 1000,
|
||||
// Might need to be changed
|
||||
label: isWidth ? context.loc.width : context.loc.height,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
onChanged(val);
|
||||
_resizeImage();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _heightSlider() {
|
||||
return _slider(
|
||||
isWidth: false,
|
||||
onChanged: (value) {
|
||||
_height = value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _widthSlider() {
|
||||
return _slider(
|
||||
isWidth: true,
|
||||
onChanged: (value) {
|
||||
_width = value;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _scheduled = false;
|
||||
|
||||
void _resizeImage() {
|
||||
if (_scheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_scheduled = true;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onImageResize(_width, _height);
|
||||
_scheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,46 +1 @@
|
||||
import 'package:flutter/widgets.dart' show GlobalKey, Widget;
|
||||
import 'package:meta/meta.dart' show experimental, immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorVideoEmbedConfig {
|
||||
const QuillEditorVideoEmbedConfig({
|
||||
this.onVideoInit,
|
||||
this.customVideoBuilder,
|
||||
});
|
||||
|
||||
/// [onVideoInit] is a callback function that gets triggered when
|
||||
/// a video is initialized.
|
||||
/// You can use this to perform actions or setup configurations related
|
||||
/// to video embedding.
|
||||
///
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// onVideoInit: (videoContainerKey) {
|
||||
/// // Custom video initialization logic
|
||||
/// },
|
||||
/// // Customize other callback functions as needed
|
||||
/// ```
|
||||
final void Function(GlobalKey videoContainerKey)? onVideoInit;
|
||||
|
||||
/// [customVideoBuilder] is a callback function that receives the
|
||||
/// video URL and a read-only flag. This allows users to define
|
||||
/// their own logic for rendering video widgets, enabling support
|
||||
/// for various video platforms, such as YouTube.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// customVideoBuilder: (videoUrl, readOnly) {
|
||||
/// // Return `null` to fallback to defualt logic of QuillEditorVideoEmbedBuilder
|
||||
///
|
||||
/// // Return a custom video widget based on the videoUrl
|
||||
/// return CustomVideoWidget(videoUrl: videoUrl, readOnly: readOnly);
|
||||
/// },
|
||||
/// ```
|
||||
///
|
||||
/// It's a quick solution as response to https://github.com/singerdmx/flutter-quill/issues/2284
|
||||
///
|
||||
/// **Might be removed or changed in future releases.**
|
||||
@experimental
|
||||
final Widget? Function(String videoUrl, bool readOnly)? customVideoBuilder;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,6 +1 @@
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
@immutable
|
||||
class QuillEditorWebVideoEmbedConfig {
|
||||
const QuillEditorWebVideoEmbedConfig();
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,55 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_utils.dart';
|
||||
import 'config/video_config.dart';
|
||||
import 'widgets/video_app.dart';
|
||||
|
||||
class QuillEditorVideoEmbedBuilder extends EmbedBuilder {
|
||||
const QuillEditorVideoEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
|
||||
final QuillEditorVideoEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.videoType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
final videoUrl = embedContext.node.value.data;
|
||||
|
||||
final customVideoBuilder = config.customVideoBuilder;
|
||||
if (customVideoBuilder != null) {
|
||||
final videoWidget = customVideoBuilder(videoUrl, embedContext.readOnly);
|
||||
if (videoWidget != null) {
|
||||
return videoWidget;
|
||||
}
|
||||
}
|
||||
|
||||
final ((elementSize), margin, alignment) = getElementAttributes(
|
||||
embedContext.node,
|
||||
context,
|
||||
);
|
||||
|
||||
final width = elementSize.width;
|
||||
final height = elementSize.height;
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
margin: EdgeInsets.all(margin ?? 0.0),
|
||||
alignment: alignment,
|
||||
child: VideoApp(
|
||||
videoUrl: videoUrl,
|
||||
readOnly: embedContext.readOnly,
|
||||
onVideoInit: config.onVideoInit,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,55 +1 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../../common/utils/element_utils/element_web_utils.dart';
|
||||
import '../../common/utils/utils.dart';
|
||||
import '../../common/utils/web/web.dart';
|
||||
import 'config/video_web_config.dart';
|
||||
import 'youtube_video_url.dart';
|
||||
|
||||
class QuillEditorWebVideoEmbedBuilder extends EmbedBuilder {
|
||||
const QuillEditorWebVideoEmbedBuilder({
|
||||
required this.config,
|
||||
});
|
||||
|
||||
final QuillEditorWebVideoEmbedConfig config;
|
||||
|
||||
@override
|
||||
String get key => BlockEmbed.videoType;
|
||||
|
||||
@override
|
||||
bool get expanded => false;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
EmbedContext embedContext,
|
||||
) {
|
||||
var videoUrl = embedContext.node.value.data;
|
||||
if (isYouTubeUrl(videoUrl)) {
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
final youtubeID = convertVideoUrlToId(videoUrl);
|
||||
if (youtubeID != null) {
|
||||
videoUrl = 'https://www.youtube.com/embed/$youtubeID';
|
||||
}
|
||||
}
|
||||
|
||||
final (height, width, margin, alignment) =
|
||||
getWebElementAttributes(embedContext.node);
|
||||
|
||||
createHtmlIFrameElement(
|
||||
src: videoUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
alignSelf: alignment,
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: 500,
|
||||
child: HtmlElementView(
|
||||
viewType: videoUrl,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
import 'dart:io' show File;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../../common/utils/utils.dart';
|
||||
|
||||
/// Widget for playing back video
|
||||
/// Refer to https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
|
||||
class VideoApp extends StatefulWidget {
|
||||
const VideoApp({
|
||||
required this.videoUrl,
|
||||
required this.readOnly,
|
||||
super.key,
|
||||
this.onVideoInit,
|
||||
});
|
||||
|
||||
final String videoUrl;
|
||||
final bool readOnly;
|
||||
final void Function(GlobalKey videoContainerKey)? onVideoInit;
|
||||
|
||||
@override
|
||||
VideoAppState createState() => VideoAppState();
|
||||
}
|
||||
|
||||
class VideoAppState extends State<VideoApp> {
|
||||
late VideoPlayerController _controller;
|
||||
GlobalKey videoContainerKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = isHttpUrl(widget.videoUrl)
|
||||
? VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl))
|
||||
: VideoPlayerController.file(File(widget.videoUrl))
|
||||
..initialize().then((_) {
|
||||
// Ensure the first frame is shown after the video is initialized,
|
||||
// even before the play button has been pressed.
|
||||
setState(() {});
|
||||
if (widget.onVideoInit != null) {
|
||||
widget.onVideoInit?.call(videoContainerKey);
|
||||
}
|
||||
}).catchError((error) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultStyles = DefaultStyles.getInstance(context);
|
||||
if (_controller.value.hasError) {
|
||||
if (widget.readOnly) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: widget.videoUrl,
|
||||
style: defaultStyles.link,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(
|
||||
Uri.parse(widget.videoUrl),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
text: widget.videoUrl,
|
||||
style: defaultStyles.link,
|
||||
),
|
||||
);
|
||||
} else if (!_controller.value.isInitialized) {
|
||||
return VideoProgressIndicator(
|
||||
_controller,
|
||||
allowScrubbing: true,
|
||||
colors: const VideoProgressColors(playedColor: Colors.blue),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
key: videoContainerKey,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_controller.value.isPlaying
|
||||
? _controller.pause()
|
||||
: _controller.play();
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)),
|
||||
_controller.value.isPlaying
|
||||
? const SizedBox.shrink()
|
||||
: Container(
|
||||
color: const Color(0xfff5f5f5),
|
||||
child: const Icon(
|
||||
Icons.play_arrow,
|
||||
size: 60,
|
||||
color: Colors.blueGrey,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Function copied from https://github.com/sarbagyastha/youtube_player_flutter/blob/f8e1e79991066bcc70f0a7c93941ca0d54b7370e/packages/youtube_player_flutter/lib/src/player/youtube_player.dart#L154
|
||||
/// and is not written as part of this project.
|
||||
///
|
||||
/// Used as quick response for https://github.com/singerdmx/flutter-quill/issues/2284
|
||||
@experimental
|
||||
@internal
|
||||
@Deprecated(
|
||||
'Will be removed in future releases, for now included as quick response to https://github.com/singerdmx/flutter-quill/issues/2284',
|
||||
)
|
||||
String? convertVideoUrlToId(String url, {bool trimWhitespaces = true}) {
|
||||
if (!url.contains('http') && (url.length == 11)) return url;
|
||||
if (trimWhitespaces) url = url.trim();
|
||||
|
||||
for (final exp in [
|
||||
RegExp(
|
||||
r'^https:\/\/(?:www\.|m\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(
|
||||
r'^https:\/\/(?:music\.)?youtube\.com\/watch\?v=([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(
|
||||
r'^https:\/\/(?:www\.|m\.)?youtube\.com\/shorts\/([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(
|
||||
r'^https:\/\/(?:www\.|m\.)?youtube(?:-nocookie)?\.com\/embed\/([_\-a-zA-Z0-9]{11}).*$'),
|
||||
RegExp(r'^https:\/\/youtu\.be\/([_\-a-zA-Z0-9]{11}).*$')
|
||||
]) {
|
||||
final Match? match = exp.firstMatch(url);
|
||||
if (match != null && match.groupCount >= 1) return match.group(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,106 +1 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import 'editor/image/config/image_config.dart';
|
||||
import 'editor/image/image_embed.dart';
|
||||
import 'editor/video/config/video_config.dart';
|
||||
import 'editor/video/config/video_web_config.dart';
|
||||
import 'editor/video/video_embed.dart';
|
||||
import 'editor/video/video_web_embed.dart';
|
||||
import 'toolbar/camera/camera_button.dart';
|
||||
import 'toolbar/camera/config/camera_config.dart';
|
||||
import 'toolbar/image/config/image_config.dart';
|
||||
import 'toolbar/image/image_button.dart';
|
||||
import 'toolbar/video/config/video_config.dart';
|
||||
import 'toolbar/video/video_button.dart';
|
||||
|
||||
abstract final class FlutterQuillEmbeds {
|
||||
/// Returns a list of embed builders for [QuillEditor]
|
||||
/// to provide basic support for loading images and videos.
|
||||
///
|
||||
static List<EmbedBuilder> editorBuilders({
|
||||
QuillEditorImageEmbedConfig? imageEmbedConfig =
|
||||
const QuillEditorImageEmbedConfig(),
|
||||
QuillEditorVideoEmbedConfig? videoEmbedConfig =
|
||||
const QuillEditorVideoEmbedConfig(),
|
||||
}) {
|
||||
return [
|
||||
if (imageEmbedConfig != null)
|
||||
QuillEditorImageEmbedBuilder(
|
||||
config: imageEmbedConfig,
|
||||
),
|
||||
if (videoEmbedConfig != null)
|
||||
QuillEditorVideoEmbedBuilder(
|
||||
config: videoEmbedConfig,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns a list of embed builders specifically designed for web support
|
||||
/// to load images and videos.
|
||||
///
|
||||
static List<EmbedBuilder> editorWebBuilders({
|
||||
QuillEditorImageEmbedConfig? imageEmbedConfig =
|
||||
const QuillEditorImageEmbedConfig(),
|
||||
QuillEditorWebVideoEmbedConfig? videoEmbedConfig =
|
||||
const QuillEditorWebVideoEmbedConfig(),
|
||||
}) {
|
||||
if (!kIsWeb) {
|
||||
throw UnsupportedError(
|
||||
'The ${FlutterQuillEmbeds.editorWebBuilders} is for web, use ${FlutterQuillEmbeds.editorBuilders} '
|
||||
'instead for non-web platforms',
|
||||
);
|
||||
}
|
||||
return [
|
||||
if (imageEmbedConfig != null)
|
||||
QuillEditorImageEmbedBuilder(
|
||||
config: imageEmbedConfig,
|
||||
),
|
||||
if (videoEmbedConfig != null)
|
||||
QuillEditorWebVideoEmbedBuilder(
|
||||
config: videoEmbedConfig,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns a list of embed builders for [QuillEditor].
|
||||
///
|
||||
/// It will use [editorWebBuilders] for web and [editorBuilders] for non-web platforms.
|
||||
static List<EmbedBuilder> defaultEditorBuilders() {
|
||||
return kIsWeb ? editorWebBuilders() : editorBuilders();
|
||||
}
|
||||
|
||||
/// Returns a list of embed button builders to support images and videos.
|
||||
///
|
||||
/// Pass `null` to options of a button to not show it.
|
||||
static List<EmbedButtonBuilder> toolbarButtons({
|
||||
QuillToolbarImageButtonOptions? imageButtonOptions =
|
||||
const QuillToolbarImageButtonOptions(),
|
||||
QuillToolbarVideoButtonOptions? videoButtonOptions =
|
||||
const QuillToolbarVideoButtonOptions(),
|
||||
QuillToolbarCameraButtonOptions? cameraButtonOptions,
|
||||
}) =>
|
||||
[
|
||||
if (imageButtonOptions != null)
|
||||
(context, embedContext) => QuillToolbarImageButton(
|
||||
controller: embedContext.controller,
|
||||
options: imageButtonOptions,
|
||||
// ignore: invalid_use_of_internal_member
|
||||
baseOptions: embedContext.baseButtonOptions,
|
||||
),
|
||||
if (videoButtonOptions != null)
|
||||
(context, embedContext) => QuillToolbarVideoButton(
|
||||
controller: embedContext.controller,
|
||||
options: videoButtonOptions,
|
||||
// ignore: invalid_use_of_internal_member
|
||||
baseOptions: embedContext.baseButtonOptions,
|
||||
),
|
||||
if (cameraButtonOptions != null)
|
||||
(context, embedContext) => QuillToolbarCameraButton(
|
||||
controller: embedContext.controller,
|
||||
options: cameraButtonOptions,
|
||||
// ignore: invalid_use_of_internal_member
|
||||
baseOptions: embedContext.baseButtonOptions,
|
||||
),
|
||||
];
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,132 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import '../../common/default_image_insert.dart';
|
||||
import '../../common/default_video_insert.dart';
|
||||
import '../quill_simple_toolbar_api.dart';
|
||||
import 'camera_types.dart';
|
||||
import 'config/camera_config.dart';
|
||||
import 'select_camera_action.dart';
|
||||
|
||||
// ignore: invalid_use_of_internal_member
|
||||
class QuillToolbarCameraButton extends QuillToolbarBaseButtonStateless {
|
||||
const QuillToolbarCameraButton({
|
||||
required super.controller,
|
||||
QuillToolbarCameraButtonOptions? options,
|
||||
|
||||
/// Shares common options between all buttons, prefer the [options]
|
||||
/// over the [baseOptions].
|
||||
super.baseOptions,
|
||||
super.key,
|
||||
}) : _options = options,
|
||||
super(options: options);
|
||||
|
||||
final QuillToolbarCameraButtonOptions? _options;
|
||||
|
||||
@override
|
||||
QuillToolbarCameraButtonOptions? get options => _options;
|
||||
|
||||
void _sharedOnPressed(BuildContext context) {
|
||||
_onPressedHandler(
|
||||
context,
|
||||
controller,
|
||||
);
|
||||
afterButtonPressed(context);
|
||||
}
|
||||
|
||||
Future<CameraAction?> _getCameraAction(BuildContext context) async {
|
||||
final customCallback = options?.cameraConfig?.onRequestCameraActionCallback;
|
||||
if (customCallback != null) {
|
||||
return await customCallback(context);
|
||||
}
|
||||
final cameraAction = await showSelectCameraActionDialog(
|
||||
context: context,
|
||||
);
|
||||
|
||||
return cameraAction;
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(
|
||||
BuildContext context,
|
||||
QuillController controller,
|
||||
) async {
|
||||
final cameraAction = await _getCameraAction(context);
|
||||
|
||||
if (cameraAction == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cameraAction) {
|
||||
case CameraAction.video:
|
||||
final videoFile =
|
||||
await ImagePicker().pickVideo(source: ImageSource.camera);
|
||||
if (videoFile == null) {
|
||||
return;
|
||||
}
|
||||
await handleVideoInsert(
|
||||
videoFile.path,
|
||||
controller: controller,
|
||||
onVideoInsertCallback: options?.cameraConfig?.onVideoInsertCallback,
|
||||
onVideoInsertedCallback:
|
||||
options?.cameraConfig?.onVideoInsertedCallback,
|
||||
);
|
||||
case CameraAction.image:
|
||||
final imageFile =
|
||||
await ImagePicker().pickImage(source: ImageSource.camera);
|
||||
if (imageFile == null) {
|
||||
return;
|
||||
}
|
||||
await handleImageInsert(
|
||||
imageFile.path,
|
||||
controller: controller,
|
||||
onImageInsertCallback: options?.cameraConfig?.onImageInsertCallback,
|
||||
onImageInsertedCallback:
|
||||
options?.cameraConfig?.onImageInsertedCallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildButton(BuildContext context) {
|
||||
return QuillToolbarIconButton(
|
||||
icon: Icon(
|
||||
iconData(context),
|
||||
size: iconButtonFactor(context) * iconSize(context),
|
||||
),
|
||||
tooltip: tooltip(context),
|
||||
isSelected: false,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
iconTheme: iconTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||
return childBuilder?.call(
|
||||
QuillToolbarCameraButtonOptions(
|
||||
afterButtonPressed: afterButtonPressed(context),
|
||||
iconData: iconData(context),
|
||||
iconSize: iconSize(context),
|
||||
iconButtonFactor: iconButtonFactor(context),
|
||||
iconTheme: options?.iconTheme,
|
||||
tooltip: tooltip(context),
|
||||
cameraConfig: options?.cameraConfig,
|
||||
),
|
||||
QuillToolbarCameraButtonExtraOptions(
|
||||
controller: controller,
|
||||
context: context,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||
(context) => Icons.photo_camera;
|
||||
|
||||
@override
|
||||
String Function(BuildContext context) get getDefaultTooltip =>
|
||||
(context) => context.loc.camera;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,39 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BuildContext;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../editor/image/image_embed_types.dart';
|
||||
import '../video/config/video.dart';
|
||||
|
||||
enum CameraAction {
|
||||
video,
|
||||
image,
|
||||
}
|
||||
|
||||
/// When the user click the camera button, should we take a photo or record
|
||||
/// a video using the camera
|
||||
///
|
||||
/// by default will show a dialog that ask the user which option he/she wants
|
||||
typedef OnRequestCameraActionCallback = Future<CameraAction?> Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
@immutable
|
||||
class QuillToolbarCameraConfig {
|
||||
const QuillToolbarCameraConfig({
|
||||
this.onRequestCameraActionCallback,
|
||||
this.onImageInsertCallback,
|
||||
this.onImageInsertedCallback,
|
||||
this.onVideoInsertedCallback,
|
||||
this.onVideoInsertCallback,
|
||||
});
|
||||
|
||||
final OnRequestCameraActionCallback? onRequestCameraActionCallback;
|
||||
|
||||
final OnImageInsertedCallback? onImageInsertedCallback;
|
||||
|
||||
final OnImageInsertCallback? onImageInsertCallback;
|
||||
|
||||
final OnVideoInsertedCallback? onVideoInsertedCallback;
|
||||
|
||||
final OnVideoInsertCallback? onVideoInsertCallback;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,28 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import '../camera_types.dart';
|
||||
|
||||
class QuillToolbarCameraButtonExtraOptions
|
||||
extends QuillToolbarBaseButtonExtraOptions {
|
||||
const QuillToolbarCameraButtonExtraOptions({
|
||||
required super.controller,
|
||||
required super.context,
|
||||
required super.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
class QuillToolbarCameraButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||
QuillToolbarCameraButtonOptions, QuillToolbarCameraButtonExtraOptions> {
|
||||
const QuillToolbarCameraButtonOptions({
|
||||
this.cameraConfig,
|
||||
super.iconSize,
|
||||
super.iconButtonFactor,
|
||||
super.iconData,
|
||||
super.afterButtonPressed,
|
||||
super.tooltip,
|
||||
super.iconTheme,
|
||||
super.childBuilder,
|
||||
});
|
||||
|
||||
final QuillToolbarCameraConfig? cameraConfig;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'camera_types.dart';
|
||||
|
||||
class SelectCameraActionDialog extends StatelessWidget {
|
||||
const SelectCameraActionDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.loc.photo),
|
||||
subtitle: Text(
|
||||
context.loc.takeAPhotoUsingYourCamera,
|
||||
),
|
||||
leading: const Icon(Icons.photo_sharp),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(CameraAction.image),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.video),
|
||||
subtitle: Text(
|
||||
context.loc.recordAVideoUsingYourCamera,
|
||||
),
|
||||
leading: const Icon(Icons.camera),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(CameraAction.video),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<CameraAction?> showSelectCameraActionDialog({
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final imageSource = await showModalBottomSheet<CameraAction>(
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
builder: (context) => const SelectCameraActionDialog(),
|
||||
);
|
||||
return imageSource;
|
||||
}
|
||||
@ -1,39 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import '../../../editor/image/image_embed_types.dart';
|
||||
|
||||
class QuillToolbarImageButtonExtraOptions
|
||||
extends QuillToolbarBaseButtonExtraOptions {
|
||||
const QuillToolbarImageButtonExtraOptions({
|
||||
required super.controller,
|
||||
required super.context,
|
||||
required super.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class QuillToolbarImageButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||
QuillToolbarImageButtonOptions, QuillToolbarImageButtonExtraOptions> {
|
||||
const QuillToolbarImageButtonOptions({
|
||||
super.iconData,
|
||||
super.iconSize,
|
||||
super.iconButtonFactor,
|
||||
|
||||
/// specifies the tooltip text for the image button.
|
||||
super.tooltip,
|
||||
super.afterButtonPressed,
|
||||
super.childBuilder,
|
||||
super.iconTheme,
|
||||
this.dialogTheme,
|
||||
this.linkRegExp,
|
||||
this.imageButtonConfig = const QuillToolbarImageConfig(),
|
||||
});
|
||||
|
||||
final QuillDialogTheme? dialogTheme;
|
||||
|
||||
/// [imageLinkRegExp] is a regular expression to identify image links.
|
||||
final RegExp? linkRegExp;
|
||||
|
||||
final QuillToolbarImageConfig? imageButtonConfig;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,135 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../common/default_image_insert.dart';
|
||||
import '../../common/image_video_utils.dart';
|
||||
import '../../editor/image/image_embed_types.dart';
|
||||
import '../quill_simple_toolbar_api.dart';
|
||||
import 'config/image_config.dart';
|
||||
import 'select_image_source.dart';
|
||||
|
||||
// ignore: invalid_use_of_internal_member
|
||||
class QuillToolbarImageButton extends QuillToolbarBaseButtonStateless {
|
||||
const QuillToolbarImageButton({
|
||||
required super.controller,
|
||||
QuillToolbarImageButtonOptions? options,
|
||||
|
||||
/// Shares common options between all buttons, prefer the [options]
|
||||
/// over the [baseOptions].
|
||||
super.baseOptions,
|
||||
super.key,
|
||||
}) : _options = options,
|
||||
super(options: options);
|
||||
|
||||
final QuillToolbarImageButtonOptions? _options;
|
||||
|
||||
@override
|
||||
QuillToolbarImageButtonOptions? get options => _options;
|
||||
|
||||
void _sharedOnPressed(BuildContext context) {
|
||||
_onPressedHandler(context);
|
||||
afterButtonPressed(context);
|
||||
}
|
||||
|
||||
Future<void> _handleImageInsert(String imageUrl) async {
|
||||
await handleImageInsert(
|
||||
imageUrl,
|
||||
controller: controller,
|
||||
onImageInsertCallback: options?.imageButtonConfig?.onImageInsertCallback,
|
||||
onImageInsertedCallback:
|
||||
options?.imageButtonConfig?.onImageInsertedCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
final onRequestPickImage = options?.imageButtonConfig?.onRequestPickImage;
|
||||
if (onRequestPickImage != null) {
|
||||
final imageUrl = await onRequestPickImage(
|
||||
context,
|
||||
);
|
||||
if (imageUrl != null) {
|
||||
await _handleImageInsert(imageUrl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final source = await showSelectImageSourceDialog(
|
||||
context: context,
|
||||
);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final imageUrl = switch (source) {
|
||||
InsertImageSource.gallery =>
|
||||
(await ImagePicker().pickImage(source: ImageSource.gallery))?.path,
|
||||
InsertImageSource.link =>
|
||||
context.mounted ? await _typeLink(context) : null,
|
||||
InsertImageSource.camera =>
|
||||
(await ImagePicker().pickImage(source: ImageSource.camera))?.path,
|
||||
};
|
||||
if (imageUrl == null) {
|
||||
return;
|
||||
}
|
||||
if (imageUrl.trim().isNotEmpty) {
|
||||
await _handleImageInsert(imageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _typeLink(BuildContext context) async {
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => TypeLinkDialog(
|
||||
dialogTheme: options?.dialogTheme,
|
||||
linkRegExp: options?.linkRegExp,
|
||||
linkType: LinkType.image,
|
||||
),
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildButton(BuildContext context) {
|
||||
return QuillToolbarIconButton(
|
||||
icon: Icon(
|
||||
iconData(context),
|
||||
size: iconButtonFactor(context) * iconSize(context),
|
||||
),
|
||||
tooltip: tooltip(context),
|
||||
isSelected: false,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
iconTheme: iconTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||
return childBuilder?.call(
|
||||
QuillToolbarImageButtonOptions(
|
||||
afterButtonPressed: afterButtonPressed(context),
|
||||
iconData: iconData(context),
|
||||
iconSize: iconSize(context),
|
||||
iconButtonFactor: iconButtonFactor(context),
|
||||
dialogTheme: options?.dialogTheme,
|
||||
iconTheme: options?.iconTheme,
|
||||
linkRegExp: options?.linkRegExp,
|
||||
tooltip: tooltip(context),
|
||||
imageButtonConfig: options?.imageButtonConfig,
|
||||
),
|
||||
QuillToolbarImageButtonExtraOptions(
|
||||
context: context,
|
||||
controller: controller,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||
(context) => Icons.image;
|
||||
|
||||
@override
|
||||
String Function(BuildContext context) get getDefaultTooltip =>
|
||||
(context) => context.loc.insertImage;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import '../../editor/image/image_embed_types.dart';
|
||||
|
||||
class SelectImageSourceDialog extends StatelessWidget {
|
||||
const SelectImageSourceDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 200),
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.loc.gallery),
|
||||
subtitle: Text(
|
||||
context.loc.pickAPhotoFromYourGallery,
|
||||
),
|
||||
leading: const Icon(Icons.photo_sharp),
|
||||
onTap: () => Navigator.of(context).pop(InsertImageSource.gallery),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.camera),
|
||||
subtitle: Text(
|
||||
context.loc.takeAPhotoUsingYourCamera,
|
||||
),
|
||||
leading: const Icon(Icons.camera),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(InsertImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.link),
|
||||
subtitle: Text(
|
||||
context.loc.pasteAPhotoUsingALink,
|
||||
),
|
||||
leading: const Icon(Icons.link),
|
||||
onTap: () => Navigator.of(context).pop(InsertImageSource.link),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<InsertImageSource?> showSelectImageSourceDialog({
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final imageSource = await showModalBottomSheet<InsertImageSource>(
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
builder: (_) => const SelectImageSourceDialog(),
|
||||
);
|
||||
return imageSource;
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
/// APIs that are meant to be used by the `flutter_quil_extensions` only.
|
||||
///
|
||||
/// Breaking changes can be introduced from `flutter_quill` in minor versions,
|
||||
/// the `flutter_quill_extensions` will be updated and published at the same time.
|
||||
///
|
||||
/// Update both packages and use the same version for compatibility by running `flutter pub upgrade`.
|
||||
@internal
|
||||
library;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
export 'package:flutter_quill/src/toolbar/base_button/stateless_base_button.dart';
|
||||
@ -1,50 +1 @@
|
||||
import 'package:flutter/widgets.dart' show BuildContext;
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
/// When request picking an video, for example when the video button toolbar
|
||||
/// clicked, it should be null in case the user didn't choose any video or
|
||||
/// any other reasons, and it should be the video file path as string that is
|
||||
/// exists in case the user picked the video successfully
|
||||
///
|
||||
/// by default we already have a default implementation that show a dialog
|
||||
/// request the source for picking the video, from gallery, link or camera
|
||||
typedef OnRequestPickVideo = Future<String?> Function(
|
||||
BuildContext context,
|
||||
);
|
||||
|
||||
/// A callback will called when inserting a video in the editor
|
||||
/// it have the logic that will insert the video block using the controller
|
||||
typedef OnVideoInsertCallback = Future<void> Function(
|
||||
String video,
|
||||
QuillController controller,
|
||||
);
|
||||
|
||||
/// When a new video picked this callback will called and you might want to
|
||||
/// do some logic depending on your use case
|
||||
typedef OnVideoInsertedCallback = Future<void> Function(
|
||||
String video,
|
||||
);
|
||||
|
||||
enum InsertVideoSource {
|
||||
gallery,
|
||||
camera,
|
||||
link,
|
||||
}
|
||||
|
||||
/// Configurations for dealing with videos, on insert a video
|
||||
/// on request picking a video
|
||||
@immutable
|
||||
class QuillToolbarVideoConfig {
|
||||
const QuillToolbarVideoConfig({
|
||||
this.onRequestPickVideo,
|
||||
this.onVideoInsertedCallback,
|
||||
this.onVideoInsertCallback,
|
||||
});
|
||||
|
||||
final OnRequestPickVideo? onRequestPickVideo;
|
||||
|
||||
final OnVideoInsertedCallback? onVideoInsertedCallback;
|
||||
|
||||
final OnVideoInsertCallback? onVideoInsertCallback;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,32 +1 @@
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
import 'video.dart';
|
||||
|
||||
class QuillToolbarVideoButtonExtraOptions
|
||||
extends QuillToolbarBaseButtonExtraOptions {
|
||||
const QuillToolbarVideoButtonExtraOptions({
|
||||
required super.controller,
|
||||
required super.context,
|
||||
required super.onPressed,
|
||||
});
|
||||
}
|
||||
|
||||
class QuillToolbarVideoButtonOptions extends QuillToolbarBaseButtonOptions<
|
||||
QuillToolbarVideoButtonOptions, QuillToolbarVideoButtonExtraOptions> {
|
||||
const QuillToolbarVideoButtonOptions({
|
||||
this.linkRegExp,
|
||||
this.dialogTheme,
|
||||
super.iconSize,
|
||||
super.iconButtonFactor,
|
||||
super.iconData,
|
||||
super.afterButtonPressed,
|
||||
super.tooltip,
|
||||
super.iconTheme,
|
||||
super.childBuilder,
|
||||
this.videoConfig,
|
||||
});
|
||||
|
||||
final RegExp? linkRegExp;
|
||||
final QuillDialogTheme? dialogTheme;
|
||||
final QuillToolbarVideoConfig? videoConfig;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'config/video.dart';
|
||||
|
||||
class SelectVideoSourceDialog extends StatelessWidget {
|
||||
const SelectVideoSourceDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 200),
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.loc.gallery),
|
||||
subtitle: Text(
|
||||
context.loc.pickAVideoFromYourGallery,
|
||||
),
|
||||
leading: const Icon(Icons.photo_sharp),
|
||||
onTap: () => Navigator.of(context).pop(InsertVideoSource.gallery),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.camera),
|
||||
subtitle: Text(context.loc.recordAVideoUsingYourCamera),
|
||||
leading: const Icon(Icons.camera),
|
||||
enabled: !isDesktopApp,
|
||||
onTap: () => Navigator.of(context).pop(InsertVideoSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.loc.link),
|
||||
subtitle: Text(
|
||||
context.loc.pasteAVideoUsingALink,
|
||||
),
|
||||
leading: const Icon(Icons.link),
|
||||
onTap: () => Navigator.of(context).pop(InsertVideoSource.link),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<InsertVideoSource?> showSelectVideoSourceDialog({
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final imageSource = await showModalBottomSheet<InsertVideoSource>(
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
constraints: const BoxConstraints(maxWidth: 640),
|
||||
builder: (context) => const SelectVideoSourceDialog(),
|
||||
);
|
||||
return imageSource;
|
||||
}
|
||||
@ -1,134 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_quill/internal.dart';
|
||||
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../common/default_video_insert.dart';
|
||||
import '../../common/image_video_utils.dart';
|
||||
import '../quill_simple_toolbar_api.dart';
|
||||
|
||||
import 'config/video.dart';
|
||||
import 'config/video_config.dart';
|
||||
import 'select_video_source.dart';
|
||||
|
||||
// ignore: invalid_use_of_internal_member
|
||||
class QuillToolbarVideoButton extends QuillToolbarBaseButtonStateless {
|
||||
const QuillToolbarVideoButton({
|
||||
required super.controller,
|
||||
QuillToolbarVideoButtonOptions? options,
|
||||
|
||||
/// Shares common options between all buttons, prefer the [options]
|
||||
/// over the [baseOptions].
|
||||
super.baseOptions,
|
||||
super.key,
|
||||
}) : _options = options,
|
||||
super(options: options);
|
||||
|
||||
final QuillToolbarVideoButtonOptions? _options;
|
||||
|
||||
@override
|
||||
QuillToolbarVideoButtonOptions? get options => _options;
|
||||
|
||||
void _sharedOnPressed(BuildContext context) {
|
||||
_onPressedHandler(context);
|
||||
afterButtonPressed(context);
|
||||
}
|
||||
|
||||
Future<void> _handleVideoInsert(String videoUrl) async {
|
||||
await handleVideoInsert(
|
||||
videoUrl,
|
||||
controller: controller,
|
||||
onVideoInsertCallback: options?.videoConfig?.onVideoInsertCallback,
|
||||
onVideoInsertedCallback: options?.videoConfig?.onVideoInsertedCallback,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedHandler(BuildContext context) async {
|
||||
final onRequestPickVideo = options?.videoConfig?.onRequestPickVideo;
|
||||
if (onRequestPickVideo != null) {
|
||||
final videoUrl = await onRequestPickVideo(context);
|
||||
if (videoUrl != null) {
|
||||
await _handleVideoInsert(videoUrl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final imageSource = await showSelectVideoSourceDialog(context: context);
|
||||
|
||||
if (imageSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final videoUrl = switch (imageSource) {
|
||||
InsertVideoSource.gallery =>
|
||||
(await ImagePicker().pickVideo(source: ImageSource.gallery))?.path,
|
||||
InsertVideoSource.camera =>
|
||||
(await ImagePicker().pickVideo(source: ImageSource.camera))?.path,
|
||||
InsertVideoSource.link =>
|
||||
context.mounted ? await _typeLink(context) : null,
|
||||
};
|
||||
if (videoUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoUrl.trim().isNotEmpty) {
|
||||
_handleVideoInsert(videoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _typeLink(BuildContext context) async {
|
||||
final value = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (_) => TypeLinkDialog(
|
||||
dialogTheme: options?.dialogTheme,
|
||||
linkType: LinkType.video,
|
||||
),
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildButton(BuildContext context) {
|
||||
return QuillToolbarIconButton(
|
||||
icon: Icon(
|
||||
iconData(context),
|
||||
size: iconSize(context) * iconButtonFactor(context),
|
||||
),
|
||||
tooltip: tooltip(context),
|
||||
isSelected: false,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
iconTheme: iconTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? buildCustomChildBuilder(BuildContext context) {
|
||||
return childBuilder?.call(
|
||||
QuillToolbarVideoButtonOptions(
|
||||
afterButtonPressed: afterButtonPressed(context),
|
||||
iconData: iconData(context),
|
||||
dialogTheme: options?.dialogTheme,
|
||||
iconSize: iconSize(context),
|
||||
iconButtonFactor: iconButtonFactor(context),
|
||||
linkRegExp: options?.linkRegExp,
|
||||
tooltip: tooltip(context),
|
||||
iconTheme: options?.iconTheme,
|
||||
videoConfig: options?.videoConfig,
|
||||
),
|
||||
QuillToolbarVideoButtonExtraOptions(
|
||||
context: context,
|
||||
controller: controller,
|
||||
onPressed: () => _sharedOnPressed(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
IconData Function(BuildContext context) get getDefaultIconData =>
|
||||
(context) => Icons.movie_creation;
|
||||
|
||||
@override
|
||||
String Function(BuildContext context) get getDefaultTooltip =>
|
||||
(context) => context.loc.insertVideo;
|
||||
}
|
||||
// TODO Implement this library.
|
||||
|
||||
@ -1,244 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/quill_delta_sample.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const Template({super.key, required this.templateData});
|
||||
|
||||
static Template fromState(GoRouterState state) {
|
||||
return Template(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateState createState() => TemplateState();
|
||||
}
|
||||
|
||||
class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
// "org_id": orgId;
|
||||
"template_name": controllers["templateName"]?.text,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": controllers["bodyData"]?.text,
|
||||
"placeholder": [],
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor = bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}");
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(builder: (context, sizingInfo) {
|
||||
bool isDesktop = sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical: MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop, context, bodyColor, layoutColor)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop, context, Color? bodyColor, Color layoutColor) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Editor"),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteSubject(isDesktop),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.output),
|
||||
tooltip: 'Print Delta JSON to log',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'The JSON Delta has been printed to the console.')));
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
buildTempalteBody(isDesktop)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "enter the subject",
|
||||
labelStyle:
|
||||
GoogleFonts.poppins(fontSize: 12, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Content",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,19 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:delta_to_html/delta_to_html.dart';
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart' as quill;
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element, Text;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -11,18 +21,20 @@ import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/quill_delta_sample.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
import 'dialog_placeholders.dart';
|
||||
|
||||
class Template extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
@ -41,21 +53,44 @@ class TemplateState extends State<Template> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
// "org_id": orgId;
|
||||
"template_name": controllers["templateName"]?.text,
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": controllers["bodyData"]?.text,
|
||||
"placeholder": [],
|
||||
"body_html": DeltaToHTML.encodeJson(
|
||||
_controller.document.toDelta().toJson(),
|
||||
),
|
||||
// "body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
|
||||
print('start 123');
|
||||
print(jsonEncode(_controller.document.toDelta().toJson()));
|
||||
// print(jsonEncode(_controller.document));
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
@ -73,7 +108,7 @@ class TemplateState extends State<Template> {
|
||||
}
|
||||
|
||||
updateData();
|
||||
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@ -84,32 +119,255 @@ class TemplateState extends State<Template> {
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor = layoutString != null
|
||||
? 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(' ', ' ')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'"); // add more as needed
|
||||
}
|
||||
|
||||
// quill.Document convertSimpleHtmlToQuill(String htmlString) {
|
||||
// final delta = quill.Delta();
|
||||
// final doc = html_parser.parse(htmlString);
|
||||
// final body = doc.body;
|
||||
//
|
||||
// void walk(Node node) {
|
||||
// if (node is Text) {
|
||||
// delta.insert(node.text);
|
||||
// } else if (node is Element) {
|
||||
// switch (node.localName) {
|
||||
// case 'p':
|
||||
// node.nodes.forEach(walk);
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'br':
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'strong':
|
||||
// case 'b':
|
||||
// delta.insert(node.text, {'bold': true});
|
||||
// break;
|
||||
// case 'em':
|
||||
// case 'i':
|
||||
// delta.insert(node.text, {'italic': true});
|
||||
// break;
|
||||
// case 'a':
|
||||
// delta.insert(node.text, {'link': node.attributes['href']});
|
||||
// break;
|
||||
// default:
|
||||
// node.nodes.forEach(walk);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (body != null) {
|
||||
// walk(body);
|
||||
// }
|
||||
//
|
||||
// return quill.Document.fromDelta(delta..insert('\n'));
|
||||
// }
|
||||
|
||||
String formatTemplateName(String input) {
|
||||
return input
|
||||
.split('_') // split by underscore
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}");
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
String html = widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
final htmlToDelta = HtmlToDelta();
|
||||
|
||||
// Convert the HTML string to Quill Delta format
|
||||
// This is where the magic happens, but also where complex HTML might be simplified
|
||||
final quill.Delta initialDelta = htmlToDelta.convert(html);
|
||||
|
||||
// Create a Quill Document from the Delta
|
||||
final quill.Document quillDoc = quill.Document.fromDelta(initialDelta);
|
||||
|
||||
// Initialize the QuillController with the converted document
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
), // Cursor at the start
|
||||
);
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
@ -121,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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
830
lib/Screens/myTemplates/templateForex.dart
Normal file
830
lib/Screens/myTemplates/templateForex.dart
Normal file
@ -0,0 +1,830 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io show Directory, File;
|
||||
import 'package:delta_to_html/delta_to_html.dart';
|
||||
import 'package:flutter/cupertino.dart' as dom;
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart' hide Text;
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart' as quill;
|
||||
import 'package:flutter_quill_delta_from_html/flutter_quill_delta_from_html.dart';
|
||||
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
|
||||
import 'package:frontend/Screens/myTemplates/templateForex.dart'
|
||||
as _editorFocusNode;
|
||||
import 'package:frontend/Screens/myTemplates/templateForex.dart'
|
||||
as _editorScrollController;
|
||||
import 'package:frontend/Screens/myTemplates/templateForex.dart' as _controller;
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart';
|
||||
|
||||
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as dom hide Element, Text;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_quill/flutter_quill_internal.dart';
|
||||
import 'package:flutter_quill/quill_delta.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_user_travel.dart';
|
||||
import 'dialog_placeholders.dart';
|
||||
|
||||
class TemplateForex extends StatefulWidget {
|
||||
final Map<String, dynamic>? templateData;
|
||||
|
||||
const TemplateForex({super.key, required this.templateData});
|
||||
|
||||
static TemplateForex fromState(GoRouterState state) {
|
||||
return TemplateForex(templateData: state.extra as Map<String, dynamic>?);
|
||||
}
|
||||
|
||||
@override
|
||||
TemplateForexState createState() => TemplateForexState();
|
||||
}
|
||||
|
||||
class TemplateForexState extends State<TemplateForex> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
// final QuillController _controller = QuillController.basic();
|
||||
String? orgId;
|
||||
String? userId;
|
||||
Color layoutColor = Colors.redAccent;
|
||||
Color bodyColor = Colors.white;
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
List<String> dataHeader = ["subject"];
|
||||
List<String> placeholders = [];
|
||||
|
||||
late QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
late int templateId = 0;
|
||||
late String templateName = "";
|
||||
late List<Map<String, dynamic>> placeholderList = [];
|
||||
|
||||
Map<String, dynamic> get TemplateData {
|
||||
final data = {
|
||||
"org_id": orgId,
|
||||
|
||||
// "template_id": templateId,
|
||||
// "template_name": controllers["templateName"]?.text,
|
||||
"template_id": templateId,
|
||||
"template_name": templateName,
|
||||
"subject": controllers["subject"]?.text,
|
||||
"body_html": DeltaToHTML.encodeJson(
|
||||
_controller.document.toDelta().toJson(),
|
||||
),
|
||||
// "body_html": jsonEncode(_controller.document.toDelta().toJson()),
|
||||
|
||||
// "body_html": _controller,
|
||||
// "body_html": convertQuillDocToHtml(_controller.document),
|
||||
// ✅ convert delta to HTML
|
||||
"placeholder": jsonEncode(placeholderList),
|
||||
// "created_by": userId
|
||||
};
|
||||
print('start 123');
|
||||
print(jsonEncode(_controller.document.toDelta().toJson()));
|
||||
// print(jsonEncode(_controller.document));
|
||||
// Only add group_id if it's an edit operation
|
||||
// if (widget.templateData != null && widget.templateData!.containsKey('group_id')) {
|
||||
// data["template_id"] = templateData;
|
||||
// }
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
updateData();
|
||||
loadinitializeData();
|
||||
loadInitialData();
|
||||
}
|
||||
|
||||
@override
|
||||
// void dispose() {
|
||||
// // controllers.dispose();
|
||||
// // _editorScrollController.dispose();
|
||||
// _editorFocusNode.dispose();
|
||||
// super.dispose();
|
||||
// }
|
||||
void loadinitializeData() async {
|
||||
orgId = await getOrgId();
|
||||
userId = await getUserId();
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
print("convertQuillDocToHtml");
|
||||
for (final op in doc.toDelta().toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes ?? {};
|
||||
|
||||
if (insert is String) {
|
||||
var content = insert;
|
||||
|
||||
// Handle formatting (bold, italic, etc.)
|
||||
if (attrs.containsKey('bold')) {
|
||||
content = '<strong>$content</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
content = '<em>$content</em>';
|
||||
}
|
||||
|
||||
// Wrap each paragraph with <p>
|
||||
if (content.trim().isNotEmpty) {
|
||||
buffer.write('<p>${content.trim()}</p>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String convertQuillDocToHtml2(quill.Document doc) {
|
||||
final buffer = StringBuffer();
|
||||
final lines = <String>[];
|
||||
final delta = doc.toDelta();
|
||||
|
||||
String applyStyles(String text, Map<String, dynamic>? attrs) {
|
||||
if (attrs == null) return text;
|
||||
if (attrs.containsKey('bold')) {
|
||||
text = '<strong>$text</strong>';
|
||||
}
|
||||
if (attrs.containsKey('italic')) {
|
||||
text = '<em>$text</em>';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
for (final op in delta.toList()) {
|
||||
final insert = op.data;
|
||||
final attrs = op.attributes;
|
||||
|
||||
if (insert is String) {
|
||||
final parts = insert.split('\n');
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
final part = applyStyles(parts[i], attrs);
|
||||
lines.add(part);
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
// End of line: wrap accumulated content into <p>
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
lines.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining lines
|
||||
final joined = lines.join('');
|
||||
if (joined.trim().isNotEmpty) {
|
||||
buffer.writeln('<p>${joined.trim()}</p>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String extractPlainTextFromHtml(String html) {
|
||||
final regex = RegExp(r'<p>(.*?)<\/p>', multiLine: true, dotAll: true);
|
||||
final matches = regex.allMatches(html);
|
||||
|
||||
final buffer = StringBuffer();
|
||||
for (final match in matches) {
|
||||
final text = match.group(1)?.replaceAll(RegExp(r'<[^>]*>'), '') ?? '';
|
||||
buffer.writeln(text.trim());
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
String decodeHtmlEntities(String text) {
|
||||
return text
|
||||
.replaceAll(' ', ' ')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'"); // add more as needed
|
||||
}
|
||||
|
||||
// quill.Document convertSimpleHtmlToQuill(String htmlString) {
|
||||
// final delta = quill.Delta();
|
||||
// final doc = html_parser.parse(htmlString);
|
||||
// final body = doc.body;
|
||||
//
|
||||
// void walk(Node node) {
|
||||
// if (node is Text) {
|
||||
// delta.insert(node.text);
|
||||
// } else if (node is Element) {
|
||||
// switch (node.localName) {
|
||||
// case 'p':
|
||||
// node.nodes.forEach(walk);
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'br':
|
||||
// delta.insert('\n');
|
||||
// break;
|
||||
// case 'strong':
|
||||
// case 'b':
|
||||
// delta.insert(node.text, {'bold': true});
|
||||
// break;
|
||||
// case 'em':
|
||||
// case 'i':
|
||||
// delta.insert(node.text, {'italic': true});
|
||||
// break;
|
||||
// case 'a':
|
||||
// delta.insert(node.text, {'link': node.attributes['href']});
|
||||
// break;
|
||||
// default:
|
||||
// node.nodes.forEach(walk);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (body != null) {
|
||||
// walk(body);
|
||||
// }
|
||||
//
|
||||
// return quill.Document.fromDelta(delta..insert('\n'));
|
||||
// }
|
||||
|
||||
String formatTemplateName(String input) {
|
||||
return input
|
||||
.split('_') // split by underscore
|
||||
.map(
|
||||
(word) =>
|
||||
word.isNotEmpty
|
||||
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||
: '',
|
||||
)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Future<void> updateData() async {
|
||||
// Ensure apiselectedUser is not null before printing
|
||||
if (widget.templateData != null) {
|
||||
print("API Selected User Has Data - ${widget.templateData}");
|
||||
print(
|
||||
"API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}",
|
||||
);
|
||||
setState(() {
|
||||
// ✅ Wrap in setState to update the UI
|
||||
controllers["templateName"]?.text =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
controllers["subject"]?.text =
|
||||
widget.templateData?["templateData"]?["subject"] ?? "";
|
||||
final bodyHtml =
|
||||
widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
print("bodyHtml - $bodyHtml");
|
||||
|
||||
String html = widget.templateData?["templateData"]?["body_html"] ?? "";
|
||||
|
||||
final htmlToDelta = HtmlToDelta();
|
||||
|
||||
// Convert the HTML string to Quill Delta format
|
||||
// This is where the magic happens, but also where complex HTML might be simplified
|
||||
final quill.Delta initialDelta = htmlToDelta.convert(html);
|
||||
|
||||
// Create a Quill Document from the Delta
|
||||
final quill.Document quillDoc = quill.Document.fromDelta(initialDelta);
|
||||
|
||||
// Initialize the QuillController with the converted document
|
||||
_controller = quill.QuillController(
|
||||
document: quillDoc,
|
||||
selection: const TextSelection.collapsed(
|
||||
offset: 0,
|
||||
), // Cursor at the start
|
||||
);
|
||||
|
||||
// final plainText = extractPlainTextFromHtml(bodyHtml);
|
||||
// final decodedText = decodeHtmlEntities(plainText);
|
||||
// final quillDoc = quill.Document()..insert(0, decodedText);
|
||||
// _controller = quill.QuillController(
|
||||
// document: quillDoc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
|
||||
templateName =
|
||||
widget.templateData?["templateData"]?["template_name"] ?? "";
|
||||
|
||||
print("Fetched template_name: $templateName");
|
||||
|
||||
final rawPlaceholder =
|
||||
widget.templateData?["templateData"]?["placeholder"];
|
||||
|
||||
if (rawPlaceholder is String) {
|
||||
// If it's a JSON string, decode it first
|
||||
placeholderList = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(rawPlaceholder),
|
||||
);
|
||||
} else if (rawPlaceholder is List) {
|
||||
// If it's already a list (ideal case)
|
||||
placeholderList = List<Map<String, dynamic>>.from(rawPlaceholder);
|
||||
}
|
||||
|
||||
print("Extracted placeholders: $placeholders");
|
||||
|
||||
print("Fetched placeholders: $placeholderList");
|
||||
|
||||
templateId =
|
||||
int.tryParse(
|
||||
widget.templateData?["templateData"]?["template_id"]
|
||||
?.toString() ??
|
||||
'0',
|
||||
) ??
|
||||
0;
|
||||
print("Fetched template_id: $templateId");
|
||||
|
||||
// if (widget.group?["international_policy_id"] != null) {
|
||||
// selectedInternational =
|
||||
// widget.group!["international_policy_id"].toString();
|
||||
// }
|
||||
});
|
||||
} else {
|
||||
print("API Selected User Has Data - No data available yet");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
Map<String, dynamic> data = TemplateData;
|
||||
|
||||
print('TemplateData - $data');
|
||||
// final String deltaJsonString = TemplateData?["body_html"] ?? "[]";
|
||||
//
|
||||
// print('deltaJsonString $deltaJsonString');
|
||||
//
|
||||
// // Decode the JSON string into a list
|
||||
// final List<dynamic> deltaJson = jsonDecode(deltaJsonString);
|
||||
//
|
||||
// print(DeltaToHTML.encodeJson(deltaJson));
|
||||
//
|
||||
// // Then convert it to a Quill document
|
||||
// final doc = Document.fromJson(List<Map<String, dynamic>>.from(deltaJson));
|
||||
//
|
||||
// // Set it to the controller
|
||||
// _controller = QuillController(
|
||||
// document: doc,
|
||||
// selection: const TextSelection.collapsed(offset: 0),
|
||||
// );
|
||||
// print('doc JSON: ${jsonEncode(doc.toDelta().toJson())}');
|
||||
// print('doc plain text: ${doc.toPlainText()}');
|
||||
// print('doc $doc');
|
||||
|
||||
setState(() {
|
||||
updateTemplateData(data);
|
||||
// This triggers UI rebuild with error messages
|
||||
// if (validateData()) {
|
||||
// postGroupData();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateTemplateData(policyData) async {
|
||||
final String apiUrldata = '$apiUrl/api/template/update/${templateId}';
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.put(
|
||||
Uri.parse(apiUrldata),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
context.go('/templateList');
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(
|
||||
child: buildUserTable(
|
||||
isDesktop,
|
||||
context,
|
||||
bodyColor,
|
||||
layoutColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(
|
||||
bool isDesktop,
|
||||
context,
|
||||
Color? bodyColor,
|
||||
Color layoutColor,
|
||||
) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 5.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(28),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
formatTemplateName(templateName),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
|
||||
// SizedBox(height: 10),
|
||||
// buildTempalteSubject(isDesktop),
|
||||
//
|
||||
// SizedBox(height: 10),
|
||||
buildTempalteBody(isDesktop),
|
||||
Spacer(),
|
||||
buildActions(isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteSubject(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Subject",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserTravellerWrapper(
|
||||
isFocused: false,
|
||||
color: Colors.white,
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
isDesktop: isDesktop,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||||
controller: controllers["subject"],
|
||||
onChanged: (value) {
|
||||
// _clearError("local_id_num");
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: "Enter the subject",
|
||||
labelStyle: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTempalteBody(bool isDesktop) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Text(
|
||||
// "Content",
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 12,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// color: Color(0xFF575A74),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
color: Color(0xFFFFFEF0),
|
||||
width: isDesktop ? MediaQuery.of(context).size.width * 0.8 : null,
|
||||
child: IconTheme(
|
||||
data: IconThemeData(size: 18), // Set icon size here
|
||||
child: QuillSimpleToolbar(
|
||||
controller: _controller,
|
||||
config: QuillSimpleToolbarConfig(
|
||||
embedButtons: FlutterQuillEmbeds.toolbarButtons(),
|
||||
showClipboardPaste: true,
|
||||
customButtons: [
|
||||
QuillToolbarCustomButtonOptions(
|
||||
icon: const Icon(Icons.add_alarm_rounded),
|
||||
onPressed: () {
|
||||
_controller.document.insert(
|
||||
_controller.selection.extentOffset,
|
||||
TimeStampEmbed(DateTime.now().toString()),
|
||||
);
|
||||
|
||||
_controller.updateSelection(
|
||||
TextSelection.collapsed(
|
||||
offset: _controller.selection.extentOffset + 1,
|
||||
),
|
||||
ChangeSource.local,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
buttonOptions: QuillSimpleToolbarButtonOptions(
|
||||
base: QuillToolbarBaseButtonOptions(
|
||||
afterButtonPressed: () {
|
||||
final isDesktop = {
|
||||
TargetPlatform.linux,
|
||||
TargetPlatform.windows,
|
||||
TargetPlatform.macOS,
|
||||
}.contains(defaultTargetPlatform);
|
||||
// if (isDesktop) {
|
||||
// _editorFocusNode.requestFocus();
|
||||
// }
|
||||
},
|
||||
),
|
||||
linkStyle: QuillToolbarLinkStyleButtonOptions(
|
||||
validateLink: (link) {
|
||||
// Treats all links as valid. When launching the URL,
|
||||
// `https://` is prefixed if the link is incomplete (e.g., `google.com` → `https://google.com`)
|
||||
// however this happens only within the editor.
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final selected = await showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) =>
|
||||
PlaceholdersModal(placeholders: placeholderList),
|
||||
);
|
||||
|
||||
if (selected != null) {
|
||||
print("User selected placeholder: $selected");
|
||||
// You can now insert into a controller or editor
|
||||
// Ensure the editor is focused
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
|
||||
final selection = _controller.selection;
|
||||
final position = selection.baseOffset;
|
||||
|
||||
// if (position >= 0) {
|
||||
// final intPosition = position.toInt();
|
||||
//
|
||||
// _controller.document.insert(intPosition, selected);
|
||||
//
|
||||
// _controller.updateSelection(
|
||||
// TextSelection.collapsed(
|
||||
// offset: intPosition + selected.length,
|
||||
// ),
|
||||
// ChangeSource.local,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.grey,
|
||||
// padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Placeholders',
|
||||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: MediaQuery.of(context).size.height * 0.5,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Color(0xFFD6D5E6), width: 0.5),
|
||||
),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
|
||||
focusNode: _focusNode,
|
||||
config: QuillEditorConfig(
|
||||
placeholder: 'Start writing your notes...',
|
||||
padding: const EdgeInsets.all(16),
|
||||
embedBuilders: [
|
||||
...FlutterQuillEmbeds.editorBuilders(
|
||||
imageEmbedConfig: QuillEditorImageEmbedConfig(
|
||||
imageProviderBuilder: (context, imageUrl) {
|
||||
// https://pub.dev/packages/flutter_quill_extensions#-image-assets
|
||||
if (imageUrl.startsWith('assets/')) {
|
||||
return AssetImage(imageUrl);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
videoEmbedConfig: QuillEditorVideoEmbedConfig(
|
||||
customVideoBuilder: (videoUrl, readOnly) {
|
||||
// To load YouTube videos https://github.com/singerdmx/flutter-quill/releases/tag/v10.8.0
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
TimeStampEmbedBuilder(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildActions(bool isDesktop) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.go('/OrganizationSettings');
|
||||
// You can get text from commentController.text
|
||||
Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: layoutColor, width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: layoutColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: layoutColor,
|
||||
// backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(fontSize: 14, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_editorScrollController.dispose();
|
||||
_editorFocusNode.dispose();
|
||||
}
|
||||
|
||||
class TimeStampEmbed extends Embeddable {
|
||||
const TimeStampEmbed(String value) : super(timeStampType, value);
|
||||
|
||||
static const String timeStampType = 'timeStamp';
|
||||
|
||||
static TimeStampEmbed fromDocument(Document document) =>
|
||||
TimeStampEmbed(jsonEncode(document.toDelta().toJson()));
|
||||
|
||||
Document get document => Document.fromJson(jsonDecode(data));
|
||||
}
|
||||
|
||||
class TimeStampEmbedBuilder extends EmbedBuilder {
|
||||
@override
|
||||
String get key => 'timeStamp';
|
||||
|
||||
@override
|
||||
String toPlainText(Embed node) {
|
||||
return node.value.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, EmbedContext embedContext) {
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time_rounded),
|
||||
Text(embedContext.node.value.data as String),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/Screens/myTemplates/templateTest.dart
Normal file
37
lib/Screens/myTemplates/templateTest.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
MyHomePageState createState() => MyHomePageState();
|
||||
}
|
||||
|
||||
class MyHomePageState extends State<MyHomePage> {
|
||||
final QuillController _controller = QuillController.basic();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("title")),
|
||||
body: Column(
|
||||
children: [
|
||||
QuillSimpleToolbar(controller: _controller),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: QuillEditor(
|
||||
controller: _controller,
|
||||
scrollController: ScrollController(),
|
||||
focusNode: _focusNode,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
@ -66,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)),
|
||||
),
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
@ -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
@ -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");
|
||||
});
|
||||
|
||||
|
||||
428
lib/Screens/policy/policyListBackup.dart
Normal file
428
lib/Screens/policy/policyListBackup.dart
Normal file
@ -0,0 +1,428 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend/Screens/group/group.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
|
||||
class PolicyListBackup extends StatefulWidget {
|
||||
@override
|
||||
_PolicyListBackupState createState() => _PolicyListBackupState();
|
||||
}
|
||||
|
||||
class _PolicyListBackupState extends State<PolicyListBackup> {
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
List<dynamic>? apiAllGroups;
|
||||
Color? layoutColor;
|
||||
Color? bodyColor;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadAllGroups();
|
||||
loadInitialData();
|
||||
});
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> loadAllGroups() async {
|
||||
try {
|
||||
final result = await apiService.fetchAllPolicy();
|
||||
|
||||
// Sort by policy_id descending (latest first)
|
||||
result.sort((a, b) {
|
||||
int idA = int.tryParse(a['policy_id'].toString()) ?? 0;
|
||||
int idB = int.tryParse(b['policy_id'].toString()) ?? 0;
|
||||
return idB.compareTo(idA); // latest first
|
||||
});
|
||||
|
||||
setState(() {
|
||||
apiAllGroups = result;
|
||||
});
|
||||
print("Fetched services: $apiAllGroups");
|
||||
} catch (e) {
|
||||
print('Error fetching role list: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void handleActiveStatus(
|
||||
Map<String, dynamic> policyData,
|
||||
String policyId,
|
||||
String currentStatus,
|
||||
) async {
|
||||
print("Toggling user status - $policyId (Current: $currentStatus)");
|
||||
|
||||
final String apiUrlData =
|
||||
'$apiUrl/api/policy/createOrUpdate'; // API for updating user
|
||||
final String? token = await getToken();
|
||||
|
||||
if (token == null) {
|
||||
print("Error: Token not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle status: If active ("1"), set to inactive ("0"); otherwise, activate ("1")
|
||||
String newStatus = (currentStatus == "1") ? "0" : "1";
|
||||
|
||||
print("STatus 1 - $newStatus");
|
||||
|
||||
final int? selectedPolicyId;
|
||||
|
||||
if (policyId.isNotEmpty) {
|
||||
selectedPolicyId = int.tryParse(policyId);
|
||||
policyData['policy_id'] = selectedPolicyId; // Add only if updating
|
||||
policyData['is_active'] = newStatus; // Add only if updating
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse(apiUrlData),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(policyData), // Convert map to JSON
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print("policyData submitted successfully!");
|
||||
print("Response: ${response.body}");
|
||||
|
||||
loadAllGroups();
|
||||
} else {
|
||||
print("Failed to submit policyData. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting policyData: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void deletePolicy(Map<String, dynamic> policydata, policyId, status) {
|
||||
print("policyId : $policyId");
|
||||
print("policystatus: $status");
|
||||
print("policysData: $policydata");
|
||||
|
||||
// handleActiveStatus(groupdata, groupId, status);
|
||||
print("Calling handleActiveStatus with: id=$policyId, status=$status");
|
||||
handleActiveStatus(policydata, policyId.toString(), status.toString());
|
||||
}
|
||||
|
||||
// Future<void> deleteGroupFromApi(int groupId) async {
|
||||
// try {
|
||||
// await apiService.deleteGroup(groupId); // your delete API call
|
||||
// deleteGroup(groupId); // remove from UI list
|
||||
// } catch (e) {
|
||||
// print('Error deleting group: $e');
|
||||
// }
|
||||
// }'
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
Expanded(child: buildGroupList(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
// decoration: BoxDecoration(
|
||||
// // color: Colors.amber,
|
||||
// color: Color(0xFFE1F5FE),
|
||||
// // color: bodyColor,
|
||||
// border: Border.all(
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// color: Colors.white,
|
||||
// width: 3.5)),
|
||||
child: buildGroupListLayout(isDesktop),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupListLayout(bool isDesktop) {
|
||||
return Container(
|
||||
// margin: isDesktop
|
||||
// ? EdgeInsets.all(10.0)
|
||||
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
|
||||
// padding: const EdgeInsets.all(10),
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
// decoration: BoxDecoration(
|
||||
// border: isDesktop
|
||||
// ? Border.all(
|
||||
// width: 2,
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// )
|
||||
// : null,
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
//
|
||||
// // color: Colors.amber,
|
||||
// ),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Policy List',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Color(0xFF114D8B),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// side: BorderSide(color: , width: 1),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: () async {
|
||||
// List<dynamic> users = await futureUsers;
|
||||
context.go('/Policy');
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'New Policy',
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Icon(Icons.add_circle_outline_rounded, color: Colors.white),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
padding: const EdgeInsets.all(10),
|
||||
// margin: const EdgeInsets.only(bottom: 10),
|
||||
color: Colors.white,
|
||||
// color: Colors.red.shade100,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Column(children: [buildGroupListView(isDesktop)]),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Widget buildGroupListView(bool isDesktop) {
|
||||
// return Container(
|
||||
// child: Text("DAta"),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget buildGroupListView(bool isDesktop) {
|
||||
if (apiAllGroups == null || apiAllGroups!.isEmpty) {
|
||||
return Center(child: Text("No Policy Found."));
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: apiAllGroups!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final policy = apiAllGroups![index];
|
||||
return Card(
|
||||
// color: bodyColor,
|
||||
// color: Color(0xFFF5F5F5),
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(vertical: 6, horizontal: 10),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"Policy Name",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"Policy Type",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(flex: 1, child: Text("${policy['created_by']}")),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
"${policy['name']}",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
policy['domestic'] == "1"
|
||||
? "Domestic"
|
||||
: "International",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Expanded(flex: 1, child: Text("${policy['created_by']}")),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final rawId = policy['policy_id'];
|
||||
final intPolicyId =
|
||||
rawId is int
|
||||
? rawId
|
||||
: int.tryParse(rawId.toString()) ?? 0;
|
||||
|
||||
Map<String, dynamic> policyData = await apiService
|
||||
.getSinglePolicy(intPolicyId);
|
||||
|
||||
print("PolicyDATa: $policyData");
|
||||
|
||||
context.go("/Policy", extra: policyData);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final idStr = policy['policy_id'];
|
||||
final id = int.tryParse(idStr.toString());
|
||||
|
||||
if (id == null) {
|
||||
print("group_id is null");
|
||||
return;
|
||||
}
|
||||
final status = policy['is_active'];
|
||||
|
||||
deletePolicy(policy, id, status);
|
||||
},
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/delete.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
542
lib/Screens/traveller/travellerDetails.dart
Normal file
542
lib/Screens/traveller/travellerDetails.dart
Normal file
@ -0,0 +1,542 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../widgets/custom_text_forex.dart';
|
||||
import 'travellerList.dart';
|
||||
|
||||
class TravellerData extends StatefulWidget {
|
||||
final Future<List<dynamic>> Function() fetchGetTraveller;
|
||||
final bool isDesktop;
|
||||
final Color? layoutColor;
|
||||
|
||||
final int? travellerId; // <-- Add this
|
||||
final Map<String, dynamic>? travellerData;
|
||||
|
||||
const TravellerData({
|
||||
super.key,
|
||||
required this.isDesktop,
|
||||
this.layoutColor,
|
||||
required this.fetchGetTraveller,
|
||||
this.travellerId,
|
||||
this.travellerData,
|
||||
});
|
||||
|
||||
@override
|
||||
TravellerDataState createState() => TravellerDataState();
|
||||
}
|
||||
|
||||
class TravellerDataState extends State<TravellerData> {
|
||||
final ApiService apiService = ApiService();
|
||||
Map<String, dynamic>? apiData;
|
||||
|
||||
final Map<String, FocusNode> focusNodes = {
|
||||
"name": FocusNode(),
|
||||
"description": FocusNode(),
|
||||
};
|
||||
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
Map<String, String> errorMessages = {};
|
||||
|
||||
String? selectedName;
|
||||
String? selectedDescription;
|
||||
String? userId;
|
||||
int? travellerDataId;
|
||||
late String isActive = "1";
|
||||
|
||||
List<String> dataHeader = ["first_name", "last_name", "email", "mobile"];
|
||||
|
||||
Map<String, dynamic> travellerDetails() {
|
||||
final data = {
|
||||
// "traveller_id": int.parse(travellerId),
|
||||
"first_name": controllers["first_name"]?.text,
|
||||
"last_name": controllers["last_name"]?.text,
|
||||
"email": controllers["email"]?.text,
|
||||
"mobile": controllers["mobile"]?.text,
|
||||
"is_active": isActive,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
apiData = null;
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
if (widget.travellerId != null) {
|
||||
print('Editing D ID: ${widget.travellerId}');
|
||||
updateTravellerDetails();
|
||||
}
|
||||
}
|
||||
|
||||
void _clearError() {
|
||||
setState(() {
|
||||
errorMessages.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in controllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void updateTravellerDetails() {
|
||||
print("Inside Update Function - ${widget.travellerData}");
|
||||
|
||||
final data = widget.travellerData;
|
||||
|
||||
if (data == null) return;
|
||||
setState(() {
|
||||
controllers['first_name']?.text = data['first_name'] ?? '';
|
||||
controllers['last_name']?.text = data['last_name'] ?? '';
|
||||
controllers['email']?.text = data['email'].toString();
|
||||
controllers['mobile']?.text = data['mobile'].toString();
|
||||
isActive = data["is_active"];
|
||||
final travellerId = int.tryParse(data['traveller_id'].toString());
|
||||
travellerDataId = travellerId;
|
||||
});
|
||||
}
|
||||
|
||||
void toggleStatus() {
|
||||
setState(() {
|
||||
isActive = isActive == "1" ? "0" : "1";
|
||||
});
|
||||
}
|
||||
|
||||
bool validateData() {
|
||||
errorMessages.clear();
|
||||
|
||||
final data = {
|
||||
"first_name": controllers["first_name"]?.text,
|
||||
"last_name": controllers["last_name"]?.text,
|
||||
"email": controllers["email"]?.text,
|
||||
"mobile": controllers["mobile"]?.text,
|
||||
};
|
||||
|
||||
final requiredFields = ["first_name", "last_name", "email", "mobile"];
|
||||
bool hasFocused = false;
|
||||
|
||||
// Check validation for each field
|
||||
for (String field in requiredFields) {
|
||||
if (data[field] == null || data[field]!.trim().isEmpty) {
|
||||
errorMessages[field] = "Required";
|
||||
|
||||
if (!hasFocused) {
|
||||
focusNodes[field]?.requestFocus();
|
||||
hasFocused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data["mobile"] != null && data["mobile"].toString().isNotEmpty) {
|
||||
if (!RegExp(r"^\d{10}$").hasMatch(data["mobile"].toString())) {
|
||||
errorMessages["mobile"] =
|
||||
"Enter 10 digits"; // Invalid mobile number format
|
||||
}
|
||||
}
|
||||
|
||||
if (data["email"] != null && data["email"].toString().isNotEmpty) {
|
||||
if (!RegExp(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
).hasMatch(data["email"].toString())) {
|
||||
errorMessages["email"] = "Invalid email format"; // Invalid email format
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessages.isEmpty;
|
||||
}
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
userId = await getUserId();
|
||||
|
||||
setState(() {
|
||||
// This triggers UI rebuild with error messages
|
||||
if (validateData()) {
|
||||
postTravellerData();
|
||||
}
|
||||
});
|
||||
|
||||
final travellerData1 = travellerDetails();
|
||||
print("submit data - $travellerData1");
|
||||
}
|
||||
|
||||
Future<void> postTravellerData({int isActive = 1}) async {
|
||||
// final remarksData = getData();
|
||||
|
||||
final travellerData = travellerDetails();
|
||||
|
||||
print("initially value of the Traveller - $travellerData");
|
||||
// static here
|
||||
final orgId = await getOrgId();
|
||||
|
||||
final String apiUrldata;
|
||||
travellerData["org_id"] = orgId;
|
||||
|
||||
if (travellerDataId != null) {
|
||||
print("for edit traveller id - $travellerDataId");
|
||||
apiUrldata = '$apiUrl/api/travellers/update/$travellerDataId';
|
||||
travellerData["traveller_id"] = travellerDataId.toString();
|
||||
travellerData["updated_by"] = userId;
|
||||
(travellerData.containsKey("created_by"))
|
||||
? travellerData.remove("created_by")
|
||||
: '';
|
||||
} else {
|
||||
print("for add Traveller id - null");
|
||||
apiUrldata = '$apiUrl/api/travellers/create';
|
||||
print("called apiUrl - $apiUrldata");
|
||||
travellerData["created_by"] = userId;
|
||||
}
|
||||
print("recently Traveller data - $travellerData");
|
||||
final token = await getToken(); // Fetch token
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(apiUrldata);
|
||||
final headers = {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final body = jsonEncode(travellerData);
|
||||
|
||||
final response =
|
||||
travellerDataId != null
|
||||
? await http.put(uri, headers: headers, body: body)
|
||||
: await http.post(uri, headers: headers, body: body);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
print("Update - Response: ${response.body}");
|
||||
_clearError();
|
||||
widget.fetchGetTraveller();
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
|
||||
case 201:
|
||||
print("Save - Response: ${response.body}");
|
||||
_clearError();
|
||||
await widget.fetchGetTraveller();
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
|
||||
default:
|
||||
print("Failed to submit traveller. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting plan: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
content: SizedBox(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Row 1: Title + Edit + Delete buttons
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
(travellerDataId != null)
|
||||
? 'Edit Traveller'
|
||||
: 'Create Traveller',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 15,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(thickness: 0.2, color: Colors.blueGrey.shade100),
|
||||
const SizedBox(height: 5),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"First Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["first_name"],
|
||||
focusNode: focusNodes["first_name"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "First Name",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["first_name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["first_name"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Last Name *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["last_name"],
|
||||
focusNode: focusNodes["last_name"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Last Name",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["last_name"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["last_name"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Email *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["email"],
|
||||
focusNode: focusNodes["email"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Email",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["email"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["email"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Mobile *",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldForexWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["mobile"],
|
||||
focusNode: focusNodes["mobile"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Mobile",
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey,
|
||||
),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (errorMessages["mobile"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["mobile"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 15),
|
||||
if (travellerDataId != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Change Status ",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message:
|
||||
isActive == "1"
|
||||
? "Tap to deactivate"
|
||||
: "Tap to activate",
|
||||
child: GestureDetector(
|
||||
onTap: toggleStatus,
|
||||
child: Text(
|
||||
isActive == "1" ? "Active" : "Inactive",
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color: isActive == "1" ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (travellerDataId != null) SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
// SizedBox(
|
||||
// child: ElevatedButton(
|
||||
// onPressed: () {
|
||||
// // You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
// },
|
||||
// style: ElevatedButton.styleFrom(
|
||||
// backgroundColor: widget.layoutColor,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// ),
|
||||
// ),
|
||||
// child: Text('Cancel',
|
||||
// style: GoogleFonts.poppins(
|
||||
// fontSize: 13, color: Colors.white)),
|
||||
// ),
|
||||
// ),
|
||||
// SizedBox(
|
||||
// width: 10,
|
||||
// ),
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
// You can get text from commentController.text
|
||||
// Navigator.of(context).pop(); // Close the modal
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
848
lib/Screens/traveller/travellerList.dart
Normal file
848
lib/Screens/traveller/travellerList.dart
Normal file
@ -0,0 +1,848 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../config/apiUrl.dart';
|
||||
import '../../routes/custom_appBar.dart';
|
||||
import '../../routes/custom_drawer.dart';
|
||||
import '../../services/apiService.dart';
|
||||
import '../../utils/auth_utils.dart';
|
||||
import '../../utils/pagination.dart';
|
||||
import 'travellerDetails.dart';
|
||||
|
||||
class TravellerList extends StatefulWidget {
|
||||
const TravellerList({super.key});
|
||||
|
||||
@override
|
||||
TravellerListState createState() => TravellerListState();
|
||||
}
|
||||
|
||||
class TravellerListState extends State<TravellerList> {
|
||||
final GlobalKey<TravellerListState> travellerListKey =
|
||||
GlobalKey<TravellerListState>();
|
||||
|
||||
final ApiService apiService = ApiService();
|
||||
late Future<List<dynamic>> futureTraveller;
|
||||
|
||||
late Map<String, dynamic> depSingleData;
|
||||
String? selectedTravellerId;
|
||||
String? orgId;
|
||||
|
||||
Color? layoutColor;
|
||||
Color? bodyColor;
|
||||
|
||||
List allTraveller = [];
|
||||
List filteredTraveller = [];
|
||||
TextEditingController searchController = TextEditingController();
|
||||
|
||||
int currentPage = 0;
|
||||
int itemsPerPage = 10;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
futureTraveller = fetchGetTraveller();
|
||||
|
||||
futureTraveller.then((object) {
|
||||
setState(() {
|
||||
allTraveller = object;
|
||||
});
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
loadInitialData();
|
||||
});
|
||||
|
||||
// futurePlans = fetchPlans();
|
||||
}
|
||||
|
||||
void loadInitialData() async {
|
||||
String? layoutString = await getLayoutColor();
|
||||
String? bodyStringColor = await getBodyColor();
|
||||
|
||||
setState(() {
|
||||
layoutColor =
|
||||
layoutString != null
|
||||
? Color(int.parse(layoutString))
|
||||
: Colors.redAccent;
|
||||
|
||||
bodyColor =
|
||||
bodyStringColor != null
|
||||
? Color(int.parse(bodyStringColor))
|
||||
: Colors.white;
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString('auth_token');
|
||||
}
|
||||
|
||||
Future<List<dynamic>> refreshData() {
|
||||
print("Calling Refresh Data");
|
||||
|
||||
futureTraveller = fetchGetTraveller();
|
||||
|
||||
return futureTraveller.then((object) {
|
||||
print("Calling Refresh Data $object");
|
||||
setState(() {
|
||||
allTraveller = object;
|
||||
});
|
||||
return object;
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchGetTraveller() async {
|
||||
String? ordId = await getOrgId();
|
||||
final String apiUrlData =
|
||||
'$apiUrl/api/travellers?for=table_view&org_id=$ordId';
|
||||
|
||||
final String? token = await getToken();
|
||||
|
||||
print("Fetch Traveller");
|
||||
print("2KN Here : $token");
|
||||
|
||||
if (token == null) {
|
||||
throw Exception('Token not found. Please log in.');
|
||||
}
|
||||
|
||||
final response = await http.get(
|
||||
Uri.parse(apiUrlData),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
print("called api : $apiUrlData");
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return data['data']; // Returning raw JSON list
|
||||
} else {
|
||||
throw Exception('Failed to load users');
|
||||
}
|
||||
}
|
||||
|
||||
void filterTraveller(String query) {
|
||||
// print("all before filtering: $query");
|
||||
// final lowerQuery = query.toLowerCase();
|
||||
// setState(() {
|
||||
// filteredTraveller = allTraveller.where((object) {
|
||||
// return (object['traveller_id']?.toLowerCase().contains(lowerQuery) ??
|
||||
// false) ||
|
||||
// (object['description']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
// (object['user']?.toLowerCase().contains(lowerQuery) ?? false) ||
|
||||
// (object['is_active']?.toLowerCase().contains(lowerQuery) ?? false);
|
||||
// }).toList();
|
||||
// });
|
||||
// print("filteredPlans: $filteredTraveller");
|
||||
|
||||
print("all before filtering: $query");
|
||||
final lowerQuery = query.toLowerCase();
|
||||
setState(() {
|
||||
filteredTraveller =
|
||||
allTraveller.where((object) {
|
||||
final isActiveStatus =
|
||||
object['is_active'] == "1" ? "active" : "inactive";
|
||||
return (object['traveller_id']?.toLowerCase().contains(
|
||||
lowerQuery,
|
||||
) ??
|
||||
false) ||
|
||||
(object['first_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['last_name']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['mobile']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(object['email']?.toLowerCase().contains(lowerQuery) ??
|
||||
false) ||
|
||||
(isActiveStatus.contains(lowerQuery));
|
||||
}).toList();
|
||||
currentPage = 0;
|
||||
});
|
||||
print("filteredTraveller: $filteredTraveller");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (context, sizingInfo) {
|
||||
bool isDesktop =
|
||||
sizingInfo.deviceScreenType == DeviceScreenType.desktop;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFf5f5f5),
|
||||
// appBar: isDesktop ? null : const CustomAppBar(title: 'User Management'),
|
||||
// drawer: isDesktop ? null : CustomDrawer(isDesktop: false),
|
||||
appBar: CustomAppBar(isDesktop: isDesktop),
|
||||
drawer: CustomDrawer(isDesktop: false),
|
||||
body: Padding(
|
||||
padding:
|
||||
isDesktop
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
MediaQuery.of(context).size.width *
|
||||
0.1, // 30% of screen width as horizontal padding
|
||||
vertical:
|
||||
MediaQuery.of(context).size.height *
|
||||
0, // 5% of screen height as vertical padding
|
||||
)
|
||||
: EdgeInsets.all(0),
|
||||
child: Row(
|
||||
children: [
|
||||
// if (isDesktop) CustomDrawer(isDesktop: true),
|
||||
// const Expanded(child: Center(child: Text("User Page Content"))),
|
||||
Expanded(child: buildGroupList(isDesktop)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGroupList(bool isDesktop) {
|
||||
return Container(
|
||||
margin: isDesktop ? const EdgeInsets.only(top: 10.0, bottom: 10.0) : null,
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
color: isDesktop ? Colors.white : Color(0xFFFCFCFC),
|
||||
),
|
||||
// decoration: BoxDecoration(
|
||||
// // color: Colors.amber,
|
||||
// // color: bodyColor,
|
||||
// color: Color(0xFFE1F5FE),
|
||||
// border: Border.all(
|
||||
// color: Colors.white,
|
||||
// // color: Color(0xFFF7F7FB),
|
||||
// width: 3.5)),
|
||||
child: buildUserTable(isDesktop),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildUserTable(bool isDesktop) {
|
||||
return Container(
|
||||
// margin: isDesktop
|
||||
// ? EdgeInsets.all(10.0)
|
||||
// : EdgeInsets.only(left: 20.0, bottom: 20.0, top: 10.0, right: 20.0),
|
||||
// padding: const EdgeInsets.all(10),
|
||||
height:
|
||||
isDesktop
|
||||
? MediaQuery.of(context).size.height * 0.98
|
||||
: MediaQuery.of(context).size.height,
|
||||
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Divider(
|
||||
// thickness: 0.2, // how "thick" the line is
|
||||
// color: Colors.grey, // optional
|
||||
// ),
|
||||
Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Traveller Details',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.16),
|
||||
|
||||
if (isDesktop)
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.2,
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: filterTraveller,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
size: 18,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
Spacer(),
|
||||
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF114D8B),
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: Color(0xFF114D8B),
|
||||
disabledForegroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Color(0xFF114D8B), width: 2),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => TravellerData(
|
||||
isDesktop: isDesktop,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetTraveller: refreshData,
|
||||
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize:
|
||||
MainAxisSize.min, // Ensures content fits nicely
|
||||
children: [
|
||||
Text(
|
||||
"Add Traveller",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: isDesktop ? 13 : 11,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8), // spacing between icon and text
|
||||
Icon(
|
||||
Icons.add_circle_outline_rounded,
|
||||
size: 15,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (!isDesktop) SizedBox(height: 5),
|
||||
isDesktop
|
||||
? SizedBox.shrink()
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
height: 35,
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
onChanged: filterTraveller,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search ...",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF9E9DBD),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF9E9DBD),
|
||||
size: 18,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade200,
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: GoogleFonts.poppins(fontSize: 12),
|
||||
),
|
||||
),
|
||||
// SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder<List<dynamic>>(
|
||||
future: futureTraveller,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (snapshot.hasError ||
|
||||
!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"No Traveller Available ",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Please Create Traveller Details",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
/* Here collect the list to displayed the data in table or card Used */
|
||||
List<dynamic> object =
|
||||
filteredTraveller.isNotEmpty
|
||||
? filteredTraveller
|
||||
: allTraveller;
|
||||
|
||||
/* List is Sorting here */
|
||||
object.sort((a, b) {
|
||||
DateTime dateA = DateTime.parse(a['created_on']);
|
||||
DateTime dateB = DateTime.parse(b['created_on']);
|
||||
|
||||
return dateB.compareTo(dateA); // Descending: newest first
|
||||
});
|
||||
|
||||
/* For pagination for list ... */
|
||||
List paginatedTraveller =
|
||||
object
|
||||
.skip(currentPage * itemsPerPage)
|
||||
.take(itemsPerPage)
|
||||
.toList();
|
||||
|
||||
/* Table ... */
|
||||
Widget table = LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double minWidth = isDesktop ? constraints.maxWidth : 1300;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: minWidth),
|
||||
child: DataTable(
|
||||
dividerThickness: 0.5,
|
||||
columnSpacing: isDesktop ? 24.0 : 16.0,
|
||||
border: TableBorder(
|
||||
horizontalInside: BorderSide(
|
||||
width: 0.5,
|
||||
color: Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Name',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Email',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Mobile',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Status',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataColumn(
|
||||
label: Text(
|
||||
'Actions',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
rows:
|
||||
paginatedTraveller.map((tableObject) {
|
||||
String fullName =
|
||||
'${tableObject['first_name'] ?? ''} ${tableObject['last_name'] ?? ''}';
|
||||
String travellerId =
|
||||
tableObject['traveller_id']
|
||||
.toString(); // Get user ID
|
||||
bool isSelected =
|
||||
selectedTravellerId == travellerId;
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
fullName ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['email'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['mobile'] ?? 'N/A',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
tableObject['is_active'] == "1"
|
||||
? 'Active'
|
||||
: 'Inactive',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: "Inter",
|
||||
color:
|
||||
tableObject['is_active'] == "1"
|
||||
? Colors.green
|
||||
: Colors.grey,
|
||||
),
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
GestureDetector(
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final travellerId = int.tryParse(
|
||||
tableObject['traveller_id']
|
||||
.toString(),
|
||||
);
|
||||
|
||||
if (travellerId != null) {
|
||||
print(
|
||||
"Table cell - traveller Id -- $travellerId",
|
||||
);
|
||||
final data = await apiService
|
||||
.getTravellerDetailsFind(
|
||||
travellerId,
|
||||
);
|
||||
print("TravellerId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => TravellerData(
|
||||
isDesktop: isDesktop,
|
||||
travellerId:
|
||||
travellerId, // Pass the ID
|
||||
travellerData: data,
|
||||
layoutColor: layoutColor!,
|
||||
fetchGetTraveller:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("Invalid ID");
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/* Card ... */
|
||||
Widget buildMobileCardView(List<dynamic> paginatedUser) {
|
||||
return ListView.builder(
|
||||
itemCount: paginatedUser.length,
|
||||
itemBuilder: (context, index) {
|
||||
final cardObject = paginatedUser[index];
|
||||
String fullName =
|
||||
'${cardObject['first_name'] ?? ''} ${cardObject['last_name'] ?? ''}';
|
||||
return Card(
|
||||
color: Colors.white,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Status and Employee Code
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
fullName ?? 'N/A',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
|
||||
GestureDetector(
|
||||
child: Image.asset(
|
||||
'assets/images/IconsImg/edit.png',
|
||||
width: 20,
|
||||
height: 15,
|
||||
),
|
||||
onTap: () async {
|
||||
// final userId = getUserId(user['user_id']);
|
||||
// final usersData = await getUserDetails(userId);
|
||||
//
|
||||
final travellerId = int.tryParse(
|
||||
cardObject['traveller_id'].toString(),
|
||||
);
|
||||
|
||||
if (travellerId != null) {
|
||||
print("travellerId -- $travellerId");
|
||||
final data = await apiService
|
||||
.getTravellerDetailsFind(
|
||||
travellerId,
|
||||
);
|
||||
print("TravellerId -- $data");
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => TravellerData(
|
||||
isDesktop: isDesktop,
|
||||
travellerId:
|
||||
travellerId, // Pass the ID
|
||||
travellerData: data,
|
||||
layoutColor: layoutColor!,
|
||||
// fetchGetTraveller: fetchGetTraveller,
|
||||
fetchGetTraveller:
|
||||
refreshData,
|
||||
// role:
|
||||
// "Travel Agent"
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("Invalid ID");
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: 2),
|
||||
// Trip Id and Trip Name
|
||||
// Name
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${cardObject['email'] ?? 'N/A'}',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${cardObject['mobile'] ?? 'N/A'}',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// Actions
|
||||
// Actions
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
isDesktop
|
||||
? (searchController.text.isNotEmpty &&
|
||||
filteredTraveller.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: table,
|
||||
))
|
||||
: (searchController.text.isNotEmpty &&
|
||||
filteredTraveller.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No matches found",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
)
|
||||
: buildMobileCardView(
|
||||
paginatedTraveller,
|
||||
)),
|
||||
),
|
||||
// Expanded(
|
||||
// child: isDesktop
|
||||
// ? SingleChildScrollView(
|
||||
// scrollDirection: Axis.vertical,
|
||||
// child: table, // <-- your existing table
|
||||
// )
|
||||
// : buildMobileCardView(paginatedTraveller),
|
||||
// ),
|
||||
PaginationControls(
|
||||
currentPage: currentPage,
|
||||
itemsPerPage: itemsPerPage,
|
||||
totalItems: object.length,
|
||||
activeColor: layoutColor, // your theme color
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
currentPage = page;
|
||||
});
|
||||
},
|
||||
onItemsPerPageChanged: (items) {
|
||||
setState(() {
|
||||
itemsPerPage = items;
|
||||
currentPage = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
383
lib/Screens/userManagement/create_user/change_password.dart
Normal file
383
lib/Screens/userManagement/create_user/change_password.dart
Normal file
@ -0,0 +1,383 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dropdown_search/dropdown_search.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../../config/apiUrl.dart';
|
||||
import '../../../services/apiService.dart';
|
||||
import '../../../utils/auth_utils.dart';
|
||||
import '../../../widgets/custom_user_form.dart';
|
||||
|
||||
|
||||
class ChangePasswordDialogData extends StatefulWidget {
|
||||
|
||||
final dynamic isDesktop;
|
||||
final dynamic layoutColor;
|
||||
final dynamic updaterUserId;
|
||||
final dynamic updaterEmail;
|
||||
|
||||
const ChangePasswordDialogData({
|
||||
super.key,
|
||||
this.isDesktop,
|
||||
this.layoutColor,
|
||||
this.updaterUserId,
|
||||
this.updaterEmail
|
||||
});
|
||||
|
||||
|
||||
@override
|
||||
ChangePasswordDialogDataState createState() => ChangePasswordDialogDataState();
|
||||
}
|
||||
|
||||
class ChangePasswordDialogDataState extends State<ChangePasswordDialogData> {
|
||||
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
final Map<String, TextEditingController> controllers = {};
|
||||
Map<String, String> errorMessages = {};
|
||||
|
||||
|
||||
String? loggeduserId;
|
||||
String? updaterUserIdForAPI;
|
||||
|
||||
List<String> dataHeader = [
|
||||
"email",
|
||||
"changePassword",
|
||||
"confirmPassword"
|
||||
];
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
//
|
||||
// for (var field in dataHeader) {
|
||||
// controllers[field] = TextEditingController();
|
||||
// }
|
||||
//
|
||||
// setState(() {
|
||||
// controllers['email']?.text = widget.updaterEmail ?? '';
|
||||
// controllers['changePassword']?.text = '';
|
||||
// controllers['confirmPassword']?.text = '';
|
||||
// });
|
||||
// }
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print("widget.updaterEmail: ${widget.updaterEmail}");
|
||||
|
||||
for (var field in dataHeader) {
|
||||
controllers[field] = TextEditingController();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
controllers['email']?.text = widget.updaterEmail ;
|
||||
controllers['changePassword']?.text = '';
|
||||
controllers['confirmPassword']?.text = '';
|
||||
updaterUserIdForAPI = widget.updaterUserId;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _clearError() {
|
||||
setState(() {
|
||||
errorMessages.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in controllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool validateData() {
|
||||
errorMessages.clear();
|
||||
|
||||
final String? email = controllers["email"]?.text;
|
||||
final String? changePassword = controllers["changePassword"]?.text;
|
||||
final String? confirmPassword = controllers["confirmPassword"]?.text;
|
||||
|
||||
// Required fields check
|
||||
if (email == null || email.trim().isEmpty) {
|
||||
errorMessages["email"] = "Required";
|
||||
}
|
||||
|
||||
if (changePassword == null || changePassword.trim().isEmpty) {
|
||||
errorMessages["changePassword"] = "Required";
|
||||
}
|
||||
|
||||
if (confirmPassword == null || confirmPassword.trim().isEmpty) {
|
||||
errorMessages["confirmPassword"] = "Required";
|
||||
}
|
||||
|
||||
// Password match check
|
||||
if ((changePassword?.isNotEmpty ?? false) &&
|
||||
(confirmPassword?.isNotEmpty ?? false) &&
|
||||
changePassword != confirmPassword) {
|
||||
errorMessages["changePassword"] = "Passwords do not match";
|
||||
errorMessages["confirmPassword"] = "Passwords do not match";
|
||||
}
|
||||
|
||||
// setState(() {}); // Update UI with any error messages
|
||||
return errorMessages.isEmpty;
|
||||
}
|
||||
|
||||
|
||||
Future<void> handleSubmit() async {
|
||||
loggeduserId = await getUserId();
|
||||
|
||||
setState(() {
|
||||
// This triggers UI rebuild with error messages
|
||||
if (validateData()) {
|
||||
postData();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Future<void> postData() async {
|
||||
// final remarksData = getData();
|
||||
print('sss$updaterUserIdForAPI');
|
||||
final loggedInUserId = await getUserId();
|
||||
|
||||
final password = controllers["changePassword"]?.text ?? '';
|
||||
final confirmPassword = controllers["confirmPassword"]?.text ?? '';
|
||||
final String apiUrldata = '$apiUrl/api/user/user-password/$updaterUserIdForAPI';
|
||||
|
||||
final token = await getToken();
|
||||
final headers = {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(apiUrldata);
|
||||
final headers = {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
final body = jsonEncode({
|
||||
"password": password,
|
||||
"updated_by": loggedInUserId,
|
||||
});
|
||||
|
||||
final response = await http.put(uri, headers: headers, body: body);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
print("Forex Details Created successfully!");
|
||||
print("Response: ${response.body}");
|
||||
_clearError();
|
||||
Navigator.of(context).pop();
|
||||
} else if (response.statusCode == 404) {
|
||||
Navigator.of(context).pop();
|
||||
final message = jsonDecode(response.body)['message'] ?? 'Unknown error';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.redAccent,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
print("Failed to submit plan. Status: ${response.statusCode}");
|
||||
print("Error: ${response.body}");
|
||||
}
|
||||
} catch (e) {
|
||||
print(" Error submitting plan: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.white,
|
||||
contentPadding: const EdgeInsets.fromLTRB(34, 30, 34, 30),
|
||||
// contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Row 1: Title + Edit + Delete buttons
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Change Password',
|
||||
style: GoogleFonts.poppins(fontSize: 15, color: Colors.black),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Divider(
|
||||
thickness: 0.2,
|
||||
color: Colors.blueGrey.shade100,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Email",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
// width: isDesktop
|
||||
// ? MediaQuery.of(context).size.width * 0.330
|
||||
// : MediaQuery.of(context).size.width * 0.66,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["email"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Email",
|
||||
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (errorMessages["email"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["email"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Change Password",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["changePassword"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Change Password",
|
||||
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (errorMessages["changePassword"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["changePassword"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Confirm Password",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF575A74)),
|
||||
),
|
||||
SizedBox(height: 5),
|
||||
CustomTextFieldUserWrapper(
|
||||
isFocused: false,
|
||||
isDesktop: widget.isDesktop,
|
||||
color: Colors.transparent,
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: controllers["confirmPassword"],
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Confirm Password",
|
||||
labelStyle: TextStyle(fontSize: 11, color: Colors.grey),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
)),
|
||||
),
|
||||
if (errorMessages["confirmPassword"] != null) ...[
|
||||
SizedBox(height: 5), // Space before error message
|
||||
Text(
|
||||
errorMessages["confirmPassword"]!,
|
||||
style: TextStyle(color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
handleSubmit();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.layoutColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text('Save',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11, color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
// : SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user