diff --git a/lib/Screens/myTemplates/templateForex.dart b/lib/Screens/myTemplates/templateForex.dart index 9fa8287..fbef283 100644 --- a/lib/Screens/myTemplates/templateForex.dart +++ b/lib/Screens/myTemplates/templateForex.dart @@ -1,4 +1,6 @@ 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; @@ -10,11 +12,14 @@ 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; +as _editorFocusNode; import 'package:frontend/Screens/myTemplates/templateForex.dart' - as _editorScrollController; +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; @@ -59,6 +64,8 @@ class TemplateForex extends StatefulWidget { class TemplateForexState extends State { final ApiService apiService = ApiService(); + Uint8List? _imageBytes; + String? selectedOrglogo; // final QuillController _controller = QuillController.basic(); String? orgId; String? userId; @@ -91,11 +98,6 @@ class TemplateForexState extends State { "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 }; @@ -194,14 +196,14 @@ class TemplateForexState extends State { setState(() { layoutColor = - layoutString != null - ? Color(int.parse(layoutString)) - : Colors.redAccent; + layoutString != null + ? Color(int.parse(layoutString)) + : Colors.redAccent; bodyColor = - bodyStringColor != null - ? Color(int.parse(bodyStringColor)) - : Colors.white; + bodyStringColor != null + ? Color(int.parse(bodyStringColor)) + : Colors.white; }); } @@ -349,10 +351,10 @@ class TemplateForexState extends State { .split('_') // split by underscore .map( (word) => - word.isNotEmpty - ? '${word[0].toUpperCase()}${word.substring(1)}' - : '', - ) + word.isNotEmpty + ? '${word[0].toUpperCase()}${word.substring(1)}' + : '', + ) .join(' '); } @@ -363,7 +365,7 @@ class TemplateForexState extends State { print( "API Selected User Has - ${widget.templateData?["templateData"]?["subject"]}", ); - setState(() { + setState(() async { // ✅ Wrap in setState to update the UI controllers["templateName"]?.text = widget.templateData?["templateData"]?["template_name"] ?? ""; @@ -408,7 +410,7 @@ class TemplateForexState extends State { print("Fetched template_name: $templateName"); final rawPlaceholder = - widget.templateData?["templateData"]?["placeholder"]; + widget.templateData?["templateData"]?["placeholder"]; if (rawPlaceholder is String) { // If it's a JSON string, decode it first @@ -427,12 +429,12 @@ class TemplateForexState extends State { templateId = int.tryParse( widget.templateData?["templateData"]?["template_id"] - ?.toString() ?? + ?.toString() ?? '0', ) ?? - 0; + 0; print("Fetched template_id: $templateId"); - + fetchSignature(); // if (widget.group?["international_policy_id"] != null) { // selectedInternational = // widget.group!["international_policy_id"].toString(); @@ -443,6 +445,34 @@ class TemplateForexState extends State { } } + Future fetchSignature() async { + final uri = Uri.parse('$apiUrl/api/getForexSignaturePath'); + final token = await getToken(); + + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $token'}, + ); + + 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 { + print("❌ Failed to fetch signature: ${response.statusCode}"); + } + } + Future handleSubmit() async { Map data = TemplateData; @@ -492,6 +522,7 @@ class TemplateForexState extends State { headers: { 'Authorization': 'Bearer $token', 'Content-Type': 'application/json', + // 'app-signature': 'ts-traveltool-2025-signature-123456', }, body: jsonEncode(policyData), // Convert map to JSON ); @@ -519,6 +550,70 @@ class TemplateForexState extends State { } } + Future _pickImage() async { + final picker = ImagePicker(); + final XFile? pickedFile = await picker.pickImage( + source: ImageSource.gallery, + ); + + if (pickedFile != null && kIsWeb) { + try { + 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 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'; + + // 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( @@ -532,16 +627,16 @@ class TemplateForexState extends State { 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), + 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), @@ -562,11 +657,11 @@ class TemplateForexState extends State { } Widget buildUserTable( - bool isDesktop, - context, - Color? bodyColor, - Color layoutColor, - ) { + 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), @@ -683,56 +778,65 @@ class TemplateForexState extends State { 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; - }, - ), - ), + 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, @@ -743,7 +847,7 @@ class TemplateForexState extends State { context: context, builder: (context) => - PlaceholdersModal(placeholders: placeholderList), + PlaceholdersModal(placeholders: placeholderList), ); if (selected != null) { @@ -782,9 +886,10 @@ class TemplateForexState extends State { ], ), ), + Container( padding: const EdgeInsets.all(16), - height: MediaQuery.of(context).size.height * 0.45, + height: MediaQuery.of(context).size.height * 0.38, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all(color: Color(0xFFD6D5E6), width: 0.5), @@ -799,15 +904,16 @@ class TemplateForexState extends State { 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; - }, - ), + // 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 @@ -820,6 +926,58 @@ class TemplateForexState extends State { ), ), ), + SizedBox(height: 4), + Container( + child: 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: Colors.amber, + child: Icon(Icons.add_a_photo, size: 10), + ), + ), + ], + ), + ), ], ); } @@ -859,17 +1017,17 @@ class TemplateForexState extends State { SizedBox( child: ElevatedButton( onPressed: - isDisable - ? null - : () async { - setState(() { - isDisable = true; - }); + isDisable + ? null + : () async { + setState(() { + isDisable = true; + }); - await handleSubmit(); - // You can get text from commentController.text - // Navigator.of(context).pop(); // Close the modal - }, + await handleSubmit(); + // You can get text from commentController.text + // Navigator.of(context).pop(); // Close the modal + }, style: ElevatedButton.styleFrom( backgroundColor: layoutColor, // backgroundColor: widget.layoutColor, @@ -924,4 +1082,4 @@ class TimeStampEmbedBuilder extends EmbedBuilder { ], ); } -} +} \ No newline at end of file diff --git a/lib/Screens/myTemplates/templateForexOld.dart b/lib/Screens/myTemplates/templateForexOld.dart new file mode 100644 index 0000000..fbef283 --- /dev/null +++ b/lib/Screens/myTemplates/templateForexOld.dart @@ -0,0 +1,1085 @@ +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_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? templateData; + + const TemplateForex({super.key, required this.templateData}); + + static TemplateForex fromState(GoRouterState state) { + return TemplateForex(templateData: state.extra as Map?); + } + + @override + TemplateForexState createState() => TemplateForexState(); +} + +class TemplateForexState extends State { + 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 controllers = {}; + List dataHeader = ["subject"]; + List placeholders = []; + + late QuillController _controller = QuillController.basic(); + final FocusNode _focusNode = FocusNode(); + + late int templateId = 0; + late String templateName = ""; + late List> placeholderList = []; + bool isDisable = false; + + Map focusNodes = {}; + Map focusStates = {}; + + Map 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 { + for (var field in dataHeader) { + controllers[field] = TextEditingController(); + } + + updateData(); + loadinitializeData(); + loadInitialData(); + } 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 = '$content'; + } + if (attrs.containsKey('italic')) { + content = '$content'; + } + + // Wrap each paragraph with

+ if (content.trim().isNotEmpty) { + buffer.write('

${content.trim()}

'); + } + } + } + + return buffer.toString(); + } + + String convertQuillDocToHtml2(quill.Document doc) { + final buffer = StringBuffer(); + final lines = []; + final delta = doc.toDelta(); + + String applyStyles(String text, Map? attrs) { + if (attrs == null) return text; + if (attrs.containsKey('bold')) { + text = '$text'; + } + if (attrs.containsKey('italic')) { + text = '$text'; + } + 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

+ final joined = lines.join(''); + if (joined.trim().isNotEmpty) { + buffer.writeln('

${joined.trim()}

'); + } + lines.clear(); + } + } + } + } + + // Add remaining lines + final joined = lines.join(''); + if (joined.trim().isNotEmpty) { + buffer.writeln('

${joined.trim()}

'); + } + + return buffer.toString(); + } + + String extractPlainTextFromHtml(String html) { + final regex = RegExp(r'

(.*?)<\/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 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>.from( + jsonDecode(rawPlaceholder), + ); + } else if (rawPlaceholder is List) { + // If it's already a list (ideal case) + placeholderList = List>.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 fetchSignature() async { + final uri = Uri.parse('$apiUrl/api/getForexSignaturePath'); + final token = await getToken(); + + final response = await http.get( + uri, + headers: {'Authorization': 'Bearer $token'}, + ); + + 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 { + print("❌ Failed to fetch signature: ${response.statusCode}"); + } + } + + Future handleSubmit() async { + Map data = TemplateData; + + print('TemplateData - $data'); + // final String deltaJsonString = TemplateData?["body_html"] ?? "[]"; + // + // print('deltaJsonString $deltaJsonString'); + // + // // Decode the JSON string into a list + // final List deltaJson = jsonDecode(deltaJsonString); + // + // print(DeltaToHTML.encodeJson(deltaJson)); + // + // // Then convert it to a Quill document + // final doc = Document.fromJson(List>.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 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 { + 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 _pickImage() async { + final picker = ImagePicker(); + final XFile? pickedFile = await picker.pickImage( + source: ImageSource.gallery, + ); + + if (pickedFile != null && kIsWeb) { + try { + 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 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'; + + // 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"], + 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: 4), + Container( + child: 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: Colors.amber, + child: Icon(Icons.add_a_photo, size: 10), + ), + ), + ], + ), + ), + ], + ); + } + + 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), + ], + ); + } +} \ No newline at end of file