1139 lines
36 KiB
Dart
1139 lines
36 KiB
Dart
import 'dart:convert';
|
|
import 'dart:html' as html;
|
|
import 'dart:async';
|
|
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/services.dart';
|
|
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:http_parser/http_parser.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:mime/mime.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, 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_breadcrumb_navigation.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();
|
|
|
|
Uint8List? _imageBytes;
|
|
String? selectedOrglogo;
|
|
// 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 = [];
|
|
bool isDisable = false;
|
|
|
|
Map<String, FocusNode> focusNodes = {};
|
|
Map<String, bool> focusStates = {};
|
|
|
|
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(),
|
|
),
|
|
"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();
|
|
_checkAuthAndLoadData();
|
|
//
|
|
// for (var field in dataHeader) {
|
|
// controllers[field] = TextEditingController();
|
|
// }
|
|
//
|
|
// updateData();
|
|
// loadinitializeData();
|
|
// loadInitialData();
|
|
for (var field in dataHeader) {
|
|
focusNodes["${field}FocusNode"] = FocusNode();
|
|
focusStates["${field}Focused"] = false;
|
|
}
|
|
|
|
for (var key in focusNodes.keys) {
|
|
_addFocusListener(focusNodes[key]!, (focus) {
|
|
setState(() {
|
|
focusStates[key.replaceFirst("FocusNode", "Focused")] = focus;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
void _checkAuthAndLoadData() async {
|
|
final String? token = await getToken(); // Your async function to get token
|
|
|
|
if (token == null || token.isEmpty) {
|
|
// Token doesn't exist → redirect to login
|
|
context.go(
|
|
"/",
|
|
); // or use: router.go("/") if you're using `GoRouter` directly
|
|
return;
|
|
}
|
|
if (!mounted) return;
|
|
try {
|
|
final roleUser = await getRoleUser();
|
|
if (roleUser != null &&
|
|
(roleUser == 'Org Admin' || roleUser == 'Travel Admin')) {
|
|
for (var field in dataHeader) {
|
|
controllers[field] = TextEditingController();
|
|
}
|
|
|
|
updateData();
|
|
loadinitializeData();
|
|
loadInitialData();
|
|
} else {
|
|
apiService.logout(context);
|
|
}
|
|
} catch (e) {
|
|
print("group : $e");
|
|
}
|
|
}
|
|
|
|
@override
|
|
// void dispose() {
|
|
// // controllers.dispose();
|
|
// // _editorScrollController.dispose();
|
|
// _editorFocusNode.dispose();
|
|
// super.dispose();
|
|
// }
|
|
void dispose() {
|
|
for (var node in focusNodes.values) {
|
|
node.dispose();
|
|
}
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
void _addFocusListener(FocusNode node, Function(bool) updateState) {
|
|
node.addListener(() {
|
|
setState(() {
|
|
updateState(node.hasFocus);
|
|
});
|
|
});
|
|
}
|
|
|
|
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(() async {
|
|
// ✅ 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");
|
|
fetchSignature();
|
|
// 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> fetchSignature() async {
|
|
final uri = Uri.parse('$apiUrl/api/getForexSignaturePath');
|
|
final token = await getToken();
|
|
|
|
final response = await http.get(
|
|
uri,
|
|
headers: {
|
|
'Authorization': 'Bearer $token',
|
|
'app-signature': 'ts-traveltool-2025-signature-123456',
|
|
},
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
print("ERS - $response");
|
|
final json = jsonDecode(response.body);
|
|
|
|
print("ERSjson - $json");
|
|
|
|
String? rawLogoPath = json['url']?.toString();
|
|
|
|
if (rawLogoPath != null && rawLogoPath.isNotEmpty) {
|
|
print("ERSrawLogoPath - $rawLogoPath");
|
|
setState(() {
|
|
selectedOrglogo = rawLogoPath;
|
|
});
|
|
}
|
|
} else if (response.statusCode == 403) {
|
|
print("403-FORB");
|
|
await apiService.logout(context);
|
|
return null;
|
|
// throw Exception('Failed to load users');
|
|
} else {
|
|
print("❌ Failed to fetch signature: ${response.statusCode}");
|
|
}
|
|
}
|
|
|
|
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(() {
|
|
isDisable = true;
|
|
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',
|
|
'app-signature': 'ts-traveltool-2025-signature-123456',
|
|
},
|
|
body: jsonEncode(policyData), // Convert map to JSON
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
print("policyData submitted successfully!");
|
|
print("Response: ${response.body}");
|
|
// context.go('/templateList');
|
|
context.go('/OrganizationSettings');
|
|
setState(() {
|
|
isDisable = false;
|
|
});
|
|
} else if (response.statusCode == 403) {
|
|
print("403-FORB");
|
|
await apiService.logout(context);
|
|
return null;
|
|
// throw Exception('Failed to load users');
|
|
} else {
|
|
print("Failed to submit policyData. Status: ${response.statusCode}");
|
|
print("Error: ${response.body}");
|
|
setState(() {
|
|
isDisable = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
print(" Error submitting policyData: $e");
|
|
setState(() {
|
|
isDisable = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _pickImage() async {
|
|
final picker = ImagePicker();
|
|
final XFile? pickedFile = await picker.pickImage(
|
|
source: ImageSource.gallery,
|
|
);
|
|
|
|
if (pickedFile != null && kIsWeb) {
|
|
try {
|
|
final allowedExtensions = ['jpg', 'jpeg', 'png'];
|
|
final fileExtension = pickedFile.name.split('.').last.toLowerCase();
|
|
|
|
if (!allowedExtensions.contains(fileExtension)) {
|
|
print('❌ Invalid file type. Please select a JPG or PNG image.');
|
|
|
|
return;
|
|
}
|
|
|
|
final bytes = await pickedFile.readAsBytes();
|
|
print('✅ Image loaded, size: ${bytes.length} bytes');
|
|
setState(() {
|
|
_imageBytes = bytes;
|
|
});
|
|
await uploadSignature();
|
|
} catch (e) {
|
|
print('❌ Error reading image bytes: $e');
|
|
}
|
|
} else {
|
|
print('⚠️ Image picking canceled or not on web.');
|
|
}
|
|
}
|
|
|
|
Future<void> uploadSignature() async {
|
|
if (_imageBytes == null) {
|
|
print('⚠️ No image selected');
|
|
return;
|
|
}
|
|
|
|
final token = await getToken(); // Fetch token
|
|
|
|
if (token == null) {
|
|
throw Exception('Token not found. Please log in.');
|
|
}
|
|
|
|
final uri = Uri.parse('$apiUrl/api/forex_signature_upload');
|
|
final request = http.MultipartRequest('POST', uri);
|
|
|
|
// Add auth header
|
|
request.headers['Authorization'] = 'Bearer $token';
|
|
request.headers['app-signature'] = 'ts-traveltool-2025-signature-123456';
|
|
|
|
// Add the image as multipart with the key "signature"
|
|
request.files.add(
|
|
http.MultipartFile.fromBytes(
|
|
'signature', // <-- key name
|
|
_imageBytes!, // <-- image bytes
|
|
filename: 'signature.png', // <-- filename (can be png/jpg)
|
|
contentType: MediaType('image', 'png'),
|
|
),
|
|
);
|
|
|
|
try {
|
|
final response = await request.send();
|
|
final respStr = await response.stream.bytesToString();
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
print('✅ Upload successful: $respStr');
|
|
} else {
|
|
print('❌ Upload failed (${response.statusCode}): $respStr');
|
|
}
|
|
} catch (e) {
|
|
print('❌ Error uploading signature: $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: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
child: BreadcrumbNavigation(
|
|
isDesktop: isDesktop,
|
|
breadcrumbItems: [
|
|
BreadcrumbItem(
|
|
title: 'Org Settings',
|
|
tooltip: 'Go To Organization Settings',
|
|
onTap: (context) {
|
|
context.go("/OrganizationSettings");
|
|
},
|
|
),
|
|
BreadcrumbItem(title: 'Forex'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 10),
|
|
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,
|
|
isFocused: focusStates["subjectFocused"] ?? 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),
|
|
focusNode: focusNodes["subjectFocusNode"],
|
|
controller: controllers["subject"],
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9 _-]')),
|
|
],
|
|
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: 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),
|
|
),
|
|
),
|
|
// 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;
|
|
// },
|
|
// ),
|
|
// ),
|
|
// ),
|
|
// ),
|
|
),
|
|
),
|
|
SizedBox(height: 2),
|
|
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.38,
|
|
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) {
|
|
// if (imageUrl.startsWith('data:image')) {
|
|
// return MemoryImage(
|
|
// base64Decode(imageUrl.split(',').last),
|
|
// );
|
|
// }
|
|
// 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(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 2),
|
|
Container(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
// mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
"Upload Signature",
|
|
style: GoogleFonts.poppins(fontSize: 11.5),
|
|
),
|
|
SizedBox(width: 5),
|
|
GestureDetector(
|
|
onTap: _pickImage,
|
|
|
|
child:
|
|
_imageBytes != null
|
|
? ClipOval(
|
|
child: Image.memory(
|
|
_imageBytes!,
|
|
// width: 50,
|
|
// height: 50,
|
|
width: 50, // Use responsive width
|
|
height: 50,
|
|
fit: BoxFit.cover,
|
|
),
|
|
)
|
|
: selectedOrglogo != null
|
|
? ClipRect(
|
|
child: Image.network(
|
|
selectedOrglogo!,
|
|
width: 50, // Use responsive width
|
|
height: 50,
|
|
// 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: Color(0xFFFFFAA0),
|
|
// backgroundColor: Color(0xFFFFFEF0),
|
|
child: Icon(Icons.add_a_photo, size: 10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(
|
|
// width: 200,
|
|
child: Text(
|
|
"* Allow types jpg, jpeg, png",
|
|
// maxLines: 2,
|
|
// softWrap: true,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w400,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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:
|
|
isDisable
|
|
? null
|
|
: () async {
|
|
setState(() {
|
|
isDisable = true;
|
|
});
|
|
|
|
await 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),
|
|
],
|
|
);
|
|
}
|
|
}
|