@visns-studio/visns-components 5.11.6 → 5.11.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,8 @@ import { saveAs } from 'file-saver';
9
9
  import { toast } from 'react-toastify';
10
10
  import { arrayMoveImmutable } from 'array-move';
11
11
  import { showConfirmDialog } from './ConfirmationDialog';
12
- import { FileText, DollarSign, Clock, Eye, X } from 'lucide-react';
12
+ import { FileText, DollarSign, Clock, Eye, X, Edit3, FileEdit, Save, Settings } from 'lucide-react';
13
+ import { Editor } from '@tinymce/tinymce-react';
13
14
 
14
15
  import CustomFetch from '../../crm/Fetch';
15
16
  import ActionButtons from './ActionButtons';
@@ -28,14 +29,16 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
28
29
  const [data, setData] = useState({});
29
30
  const [actions, setActions] = useState(actionItems || []);
30
31
 
31
- // Proposal system state (backward compatible)
32
- const [proposalMode, setProposalMode] = useState(false);
32
+ // Proposal system state (always active)
33
33
  const [proposalTemplates, setProposalTemplates] = useState([]);
34
- const [brandingProfiles, setBrandingProfiles] = useState([]);
35
34
  const [selectedTemplate, setSelectedTemplate] = useState(null);
36
- const [selectedBranding, setSelectedBranding] = useState(null);
37
35
  const [proposalPreview, setProposalPreview] = useState(null);
38
36
  const [isGeneratingProposal, setIsGeneratingProposal] = useState(false);
37
+
38
+ // Content editor state
39
+ const [showContentEditor, setShowContentEditor] = useState(false);
40
+ const [proposalContent, setProposalContent] = useState('');
41
+ const [contentSectionType, setContentSectionType] = useState('content');
39
42
 
40
43
  // Modal state variables
41
44
  const [modalShow, setModalShow] = useState(false);
@@ -504,55 +507,149 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
504
507
  if (!proposalConfig) return;
505
508
 
506
509
  try {
507
- // Load proposal templates
508
- if (proposalConfig.templateEndpoint) {
509
- const templatesRes = await CustomFetch(
510
- `${proposalConfig.templateEndpoint}/dropdown`,
511
- 'POST',
512
- {}
513
- );
514
- if (templatesRes.success) {
515
- setProposalTemplates(templatesRes.data);
516
-
517
- // Set default template if available
518
- const defaultTemplate = templatesRes.data.find(
519
- t => t.name?.includes('Standard') || t.name?.includes('Default')
510
+ console.log('Loading proposal templates...');
511
+ // Load proposal templates using dropdown endpoint
512
+ const templatesRes = await CustomFetch(
513
+ '/ajax/proposalTemplates/dropdown',
514
+ 'POST',
515
+ {}
516
+ );
517
+
518
+ console.log('Templates response:', templatesRes);
519
+
520
+ // Handle different response formats
521
+ let templates = [];
522
+ if (templatesRes.data && templatesRes.data.data && Array.isArray(templatesRes.data.data)) {
523
+ templates = templatesRes.data.data;
524
+ } else if (templatesRes.data && Array.isArray(templatesRes.data)) {
525
+ templates = templatesRes.data;
526
+ } else if (Array.isArray(templatesRes)) {
527
+ templates = templatesRes;
528
+ } else if (templatesRes.success && templatesRes.data) {
529
+ templates = templatesRes.data;
530
+ }
531
+
532
+ console.log('Extracted templates:', templates);
533
+
534
+ if (templates && templates.length > 0) {
535
+ setProposalTemplates(templates);
536
+
537
+ // Check if quote already has a saved proposal template
538
+ if (data.proposal_template_id) {
539
+ const savedTemplate = templates.find(t => t.id == data.proposal_template_id);
540
+ if (savedTemplate) {
541
+ console.log('Setting previously saved template:', savedTemplate);
542
+ setSelectedTemplate(data.proposal_template_id);
543
+ } else {
544
+ console.log('Saved template ID not found in available templates:', data.proposal_template_id);
545
+ }
546
+ } else {
547
+ // Set default template if no saved template and available
548
+ const defaultTemplate = templates.find(
549
+ t => t.label?.includes('Standard') || t.label?.includes('Default')
520
550
  );
521
551
  if (defaultTemplate) {
552
+ console.log('Setting default template:', defaultTemplate);
522
553
  setSelectedTemplate(defaultTemplate.id);
523
554
  }
524
555
  }
556
+
557
+ console.log('Templates successfully loaded:', templates.length, 'templates');
558
+ } else {
559
+ console.log('No templates found in response:', templatesRes);
525
560
  }
526
561
 
527
- // Load branding profiles
528
- if (proposalConfig.brandingEndpoint) {
529
- const brandingRes = await CustomFetch(
530
- `${proposalConfig.brandingEndpoint}/dropdown`,
531
- 'POST',
532
- {}
533
- );
534
- if (brandingRes.success) {
535
- setBrandingProfiles(brandingRes.data);
536
-
537
- // Load default branding
538
- const defaultBrandingRes = await CustomFetch(
539
- `${proposalConfig.brandingEndpoint}/default`,
540
- 'GET',
541
- {}
542
- );
543
- if (defaultBrandingRes.success) {
544
- setSelectedBranding(defaultBrandingRes.data.id);
562
+ // Branding is handled at template creation time, no need to load separately
563
+ } catch (error) {
564
+ console.error('Error loading proposal data:', error);
565
+ toast.error('Failed to load proposal templates');
566
+ }
567
+ };
568
+
569
+ const calculateGrandTotal = () => {
570
+ if (!tables || tables.length === 0) return 0;
571
+
572
+ return tables.reduce((grandTotal, table) => {
573
+ if (!data[table.id] || data[table.id].length === 0) return grandTotal;
574
+
575
+ const tableTotal = table.total
576
+ ? parseFloat(table.total)
577
+ : data[table.id].reduce((sum, row) => {
578
+ let total;
579
+ if (typeof row.total === 'number') {
580
+ total = row.total;
581
+ } else if (row.total) {
582
+ total = parseFloat(
583
+ String(row.total).replace(/[^0-9.-]+/g, '')
584
+ );
585
+ } else if (row.rate && row.qty) {
586
+ const rate = parseFloat(
587
+ String(row.rate).replace(/[^0-9.-]+/g, '')
588
+ );
589
+ const qty = parseFloat(
590
+ String(row.qty).replace(/[^0-9.-]+/g, '')
591
+ );
592
+ total = rate * qty;
593
+ } else {
594
+ total = 0;
545
595
  }
546
- }
596
+ return sum + (isNaN(total) ? 0 : total);
597
+ }, 0);
598
+
599
+ return grandTotal + (isNaN(tableTotal) ? 0 : tableTotal);
600
+ }, 0);
601
+ };
602
+
603
+ const handleTemplateSelection = async (templateId) => {
604
+ setSelectedTemplate(templateId);
605
+
606
+ if (!templateId) return;
607
+
608
+ // Save the selected template to the quote
609
+ try {
610
+ const toastId = toast.loading('Saving template selection...');
611
+
612
+ const updateData = {
613
+ proposal_template_id: templateId
614
+ };
615
+
616
+ const response = await CustomFetch(
617
+ `${page.fetchUrl}/${routeParams[urlParam]}`,
618
+ 'PUT',
619
+ updateData
620
+ );
621
+
622
+ if (response.success || response.data) {
623
+ // Update local data to reflect the change
624
+ setData(prevData => ({
625
+ ...prevData,
626
+ proposal_template_id: templateId
627
+ }));
628
+
629
+ toast.update(toastId, {
630
+ render: 'Template selection saved',
631
+ type: 'success',
632
+ isLoading: false,
633
+ autoClose: 2000,
634
+ closeButton: true,
635
+ });
636
+ } else {
637
+ throw new Error(response.message || 'Failed to save template selection');
547
638
  }
548
639
  } catch (error) {
549
- console.error('Error loading proposal data:', error);
550
- toast.error('Failed to load proposal templates and branding');
640
+ console.error('Error saving template selection:', error);
641
+ toast.error('Failed to save template selection: ' + error.message);
642
+
643
+ // Revert the selection on error
644
+ setSelectedTemplate(data.proposal_template_id || null);
551
645
  }
552
646
  };
553
647
 
554
648
  const generateProposalPreview = async () => {
555
- if (!selectedTemplate || !proposalConfig?.previewEndpoint) return;
649
+ if (!selectedTemplate) {
650
+ toast.error('Please select a template first');
651
+ return;
652
+ }
556
653
 
557
654
  setIsGeneratingProposal(true);
558
655
  const toastId = toast.loading('Generating proposal preview...');
@@ -562,30 +659,106 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
562
659
  proposal_data: {
563
660
  customer_name: data.customer?.name || 'Customer Name',
564
661
  customer_address: data.customer?.sites?.[0] ?
565
- `${data.customer.sites[0].address1 || ''} ${data.customer.sites[0].address2 || ''}, ${data.customer.sites[0].suburb || ''} ${data.customer.sites[0].state || ''} ${data.customer.sites[0].postcode || ''}`.trim() :
662
+ `${data.customer.sites[0].address1 || ''} ${data.customer.sites[0].address2 || ''}, ${data.customer.sites[0].suburb || ''} ${data.customer.sites[0].state?.initial || data.customer.sites[0].state?.name || ''} ${data.customer.sites[0].postcode || ''}`.trim() :
566
663
  'Customer Address',
567
- quote_number: data.id || 'Q-001',
568
- quote_date: data.created_at || new Date().toISOString().split('T')[0],
664
+ quote_number: String(data.id || 'Q-001'),
665
+ quote_date: data.created_at ? new Date(data.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
569
666
  current_date: new Date().toISOString().split('T')[0],
570
667
  total_amount: formatCurrency(calculateGrandTotal()),
571
668
  company_name: 'VISNS Studio',
669
+ company_address: '660C Newcastle Street, Leederville, WA, 6007',
670
+ company_phone: '08 6383 6600',
671
+ company_email: 'team@omniaglobal.com.au',
672
+ document_title: `Proposal for ${data.customer?.name || 'Customer'}`,
673
+ project_manager: 'VISNS Studio',
572
674
  terms_and_conditions: data.terms_and_condition || 'Standard terms and conditions apply.',
573
- items_onceoff: data.items_onceoff || [],
574
- items_monthly_subscription: data.items_monthly_subscription || [],
575
- items_yearly_subscription: data.items_yearly_subscription || [],
675
+ // Provide clean item data - just essential fields as simple arrays
676
+ items_onceoff: (data.items_onceoff || []).map(item => ({
677
+ description: String(item.description || ''),
678
+ rate: parseFloat(item.rate || 0),
679
+ qty: parseFloat(item.qty || 1),
680
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
681
+ })),
682
+ items_monthly_subscription: (data.items_monthly_subscription || []).map(item => ({
683
+ description: String(item.description || ''),
684
+ rate: parseFloat(item.rate || 0),
685
+ qty: parseFloat(item.qty || 1),
686
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
687
+ })),
688
+ items_yearly_subscription: (data.items_yearly_subscription || []).map(item => ({
689
+ description: String(item.description || ''),
690
+ rate: parseFloat(item.rate || 0),
691
+ qty: parseFloat(item.qty || 1),
692
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
693
+ })),
694
+ // Include saved proposal content
695
+ proposal_content: data.proposal_content || null,
696
+ proposal_section_type: data.proposal_section_type || 'content',
576
697
  },
577
- template_id: selectedTemplate,
578
- branding_id: selectedBranding,
698
+ template_id: parseInt(selectedTemplate),
699
+ download: false, // For preview
700
+ paper: 'a4',
701
+ orientation: 'portrait'
579
702
  };
580
703
 
704
+ console.log('Sending proposal data for preview:', JSON.stringify(proposalData, null, 2));
705
+
581
706
  const response = await CustomFetch(
582
- proposalConfig.previewEndpoint,
707
+ '/ajax/pdf/preview-proposal',
583
708
  'POST',
584
709
  proposalData
585
710
  );
586
711
 
587
- if (response.success) {
588
- setProposalPreview(response.html);
712
+ console.log('Preview response:', response);
713
+
714
+ if (response.data && response.data.success) {
715
+ // Add DomPDF-compatible styling to the preview HTML to match PDF output
716
+ let htmlContent = response.data.html;
717
+
718
+ // Inject CSS constants for consistent styling
719
+ const cssInjection = `
720
+ <style>
721
+ .omnia-cover-page {
722
+ background-color: #059669 !important;
723
+ color: white !important;
724
+ }
725
+ table {
726
+ border: 2px solid #333333 !important;
727
+ border-collapse: collapse !important;
728
+ }
729
+ table th {
730
+ background-color: #4fbfa5 !important;
731
+ color: white !important;
732
+ border: 1px solid #333333 !important;
733
+ }
734
+ table td {
735
+ border: 1px solid #333333 !important;
736
+ font-weight: 500 !important;
737
+ }
738
+ table tr:nth-child(even) td {
739
+ background-color: #f9f9f9 !important;
740
+ }
741
+ table tfoot tr {
742
+ background-color: #f3f4f6 !important;
743
+ }
744
+ table tfoot tr:last-child {
745
+ background-color: #333333 !important;
746
+ }
747
+ table tfoot tr:last-child td {
748
+ color: white !important;
749
+ font-weight: bold !important;
750
+ }
751
+ </style>
752
+ `;
753
+
754
+ // Insert CSS after the <head> tag or before closing </head>
755
+ if (htmlContent.includes('</head>')) {
756
+ htmlContent = htmlContent.replace('</head>', cssInjection + '</head>');
757
+ } else {
758
+ htmlContent = cssInjection + htmlContent;
759
+ }
760
+
761
+ setProposalPreview(htmlContent);
589
762
  toast.update(toastId, {
590
763
  render: 'Proposal preview generated successfully',
591
764
  type: 'success',
@@ -594,7 +767,8 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
594
767
  closeButton: true,
595
768
  });
596
769
  } else {
597
- throw new Error(response.message || 'Failed to generate preview');
770
+ console.error('Preview failed with response:', response);
771
+ throw new Error(response.data?.message || response.data?.error || 'Failed to generate preview');
598
772
  }
599
773
  } catch (error) {
600
774
  console.error('Error generating proposal preview:', error);
@@ -611,7 +785,10 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
611
785
  };
612
786
 
613
787
  const generateProposalPDF = async () => {
614
- if (!selectedTemplate || !proposalConfig?.pdfEndpoint) return;
788
+ if (!selectedTemplate) {
789
+ toast.error('Please select a template first');
790
+ return;
791
+ }
615
792
 
616
793
  setIsGeneratingProposal(true);
617
794
  const toastId = toast.loading('Generating proposal PDF...');
@@ -621,28 +798,53 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
621
798
  proposal_data: {
622
799
  customer_name: data.customer?.name || 'Customer Name',
623
800
  customer_address: data.customer?.sites?.[0] ?
624
- `${data.customer.sites[0].address1 || ''} ${data.customer.sites[0].address2 || ''}, ${data.customer.sites[0].suburb || ''} ${data.customer.sites[0].state || ''} ${data.customer.sites[0].postcode || ''}`.trim() :
801
+ `${data.customer.sites[0].address1 || ''} ${data.customer.sites[0].address2 || ''}, ${data.customer.sites[0].suburb || ''} ${data.customer.sites[0].state?.initial || data.customer.sites[0].state?.name || ''} ${data.customer.sites[0].postcode || ''}`.trim() :
625
802
  'Customer Address',
626
- quote_number: data.id || 'Q-001',
627
- quote_date: data.created_at || new Date().toISOString().split('T')[0],
803
+ quote_number: String(data.id || 'Q-001'),
804
+ quote_date: data.created_at ? new Date(data.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
628
805
  current_date: new Date().toISOString().split('T')[0],
629
806
  total_amount: formatCurrency(calculateGrandTotal()),
630
807
  company_name: 'VISNS Studio',
808
+ company_address: '660C Newcastle Street, Leederville, WA, 6007',
809
+ company_phone: '08 6383 6600',
810
+ company_email: 'team@omniaglobal.com.au',
811
+ document_title: `Proposal for ${data.customer?.name || 'Customer'}`,
812
+ project_manager: 'VISNS Studio',
631
813
  terms_and_conditions: data.terms_and_condition || 'Standard terms and conditions apply.',
632
- items_onceoff: data.items_onceoff || [],
633
- items_monthly_subscription: data.items_monthly_subscription || [],
634
- items_yearly_subscription: data.items_yearly_subscription || [],
814
+ // Provide clean item data - just essential fields as simple arrays
815
+ items_onceoff: (data.items_onceoff || []).map(item => ({
816
+ description: String(item.description || ''),
817
+ rate: parseFloat(item.rate || 0),
818
+ qty: parseFloat(item.qty || 1),
819
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
820
+ })),
821
+ items_monthly_subscription: (data.items_monthly_subscription || []).map(item => ({
822
+ description: String(item.description || ''),
823
+ rate: parseFloat(item.rate || 0),
824
+ qty: parseFloat(item.qty || 1),
825
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
826
+ })),
827
+ items_yearly_subscription: (data.items_yearly_subscription || []).map(item => ({
828
+ description: String(item.description || ''),
829
+ rate: parseFloat(item.rate || 0),
830
+ qty: parseFloat(item.qty || 1),
831
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
832
+ })),
833
+ // Include saved proposal content
834
+ proposal_content: data.proposal_content || null,
835
+ proposal_section_type: data.proposal_section_type || 'content',
635
836
  },
636
- template_id: selectedTemplate,
637
- branding_id: selectedBranding,
837
+ template_id: parseInt(selectedTemplate),
638
838
  filename: `proposal-${data.id || 'draft'}-${new Date().toISOString().split('T')[0]}.pdf`,
639
839
  download: true,
840
+ paper: 'a4',
841
+ orientation: 'portrait'
640
842
  };
641
843
 
642
844
  // Create a form for file download
643
845
  const form = document.createElement('form');
644
846
  form.method = 'POST';
645
- form.action = proposalConfig.pdfEndpoint;
847
+ form.action = '/ajax/pdf/generate-proposal';
646
848
  form.style.display = 'none';
647
849
 
648
850
  // Add CSRF token
@@ -668,12 +870,6 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
668
870
  templateInput.value = selectedTemplate;
669
871
  form.appendChild(templateInput);
670
872
 
671
- const brandingInput = document.createElement('input');
672
- brandingInput.type = 'hidden';
673
- brandingInput.name = 'branding_id';
674
- brandingInput.value = selectedBranding;
675
- form.appendChild(brandingInput);
676
-
677
873
  const filenameInput = document.createElement('input');
678
874
  filenameInput.type = 'hidden';
679
875
  filenameInput.name = 'filename';
@@ -706,15 +902,64 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
706
902
  }
707
903
  };
708
904
 
709
- const toggleProposalMode = () => {
710
- const newMode = !proposalMode;
711
- setProposalMode(newMode);
712
-
713
- if (newMode && proposalTemplates.length === 0) {
714
- loadProposalData();
905
+
906
+ const openContentEditor = () => {
907
+ // Load existing content if available
908
+ if (data.proposal_content) {
909
+ setProposalContent(data.proposal_content);
910
+ } else {
911
+ // Set default content template
912
+ setProposalContent(`
913
+ <h2>Project Overview</h2>
914
+ <p>Describe the project background and objectives here...</p>
915
+
916
+ <h2>Proposed Solution</h2>
917
+ <p>Detail your proposed solution and approach...</p>
918
+
919
+ <h2>Benefits</h2>
920
+ <ul>
921
+ <li>Key benefit 1</li>
922
+ <li>Key benefit 2</li>
923
+ <li>Key benefit 3</li>
924
+ </ul>
925
+
926
+ <h2>Implementation Timeline</h2>
927
+ <p>Outline the implementation phases and timeline...</p>
928
+ `.trim());
715
929
  }
716
-
717
- toast.info(newMode ? 'Switched to Proposal Mode' : 'Switched to Quote Mode');
930
+ setShowContentEditor(true);
931
+ };
932
+
933
+ const saveProposalContent = async () => {
934
+ try {
935
+ const response = await CustomFetch(
936
+ `/ajax/quotes/${data.id}/proposal-content`,
937
+ 'POST',
938
+ {
939
+ content: proposalContent,
940
+ section_type: contentSectionType
941
+ }
942
+ );
943
+
944
+ if (response.data && response.data.success) {
945
+ toast.success('Proposal content saved successfully');
946
+ setShowContentEditor(false);
947
+ // Update the data object with the new content
948
+ setData(prev => ({
949
+ ...prev,
950
+ proposal_content: proposalContent
951
+ }));
952
+ } else {
953
+ throw new Error(response.data?.message || 'Failed to save content');
954
+ }
955
+ } catch (error) {
956
+ console.error('Error saving proposal content:', error);
957
+ toast.error('Failed to save proposal content: ' + error.message);
958
+ }
959
+ };
960
+
961
+ const handleContentChange = (content) => {
962
+ setProposalContent(content);
718
963
  };
719
964
 
720
965
  const handleAddItem = (tableId) => {
@@ -763,9 +1008,21 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
763
1008
 
764
1009
  useEffect(() => {
765
1010
  if (actionItems && actionItems.length > 0) {
766
- setActions(actionItems);
1011
+ // Filter out proposal-related actions when proposal controls are active
1012
+ const filteredActions = proposalConfig
1013
+ ? actionItems.filter(action => {
1014
+ // Remove actions that are now handled in the proposal controls section
1015
+ const proposalActionIds = ['generateProposalPDF', 'previewProposal'];
1016
+ const proposalActionLabels = ['Generate Proposal PDF', 'Preview Proposal'];
1017
+
1018
+ return !proposalActionIds.includes(action.id) &&
1019
+ !proposalActionLabels.includes(action.label);
1020
+ })
1021
+ : actionItems;
1022
+
1023
+ setActions(filteredActions);
767
1024
  }
768
- }, [actionItems]);
1025
+ }, [actionItems, proposalConfig]);
769
1026
 
770
1027
  useEffect(() => {
771
1028
  const fetchData = async () => {
@@ -777,6 +1034,11 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
777
1034
  {}
778
1035
  );
779
1036
  setData(res.data); // Assuming res is the data to set
1037
+
1038
+ // If quote has a saved proposal template, set it as selected
1039
+ if (res.data && res.data.proposal_template_id) {
1040
+ setSelectedTemplate(res.data.proposal_template_id);
1041
+ }
780
1042
  } catch (error) {
781
1043
  console.error('Error fetching data:', error);
782
1044
  }
@@ -786,6 +1048,24 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
786
1048
  fetchData();
787
1049
  }, [routeParams['*']]);
788
1050
 
1051
+ // Load proposal data on component mount
1052
+ useEffect(() => {
1053
+ if (proposalConfig && proposalTemplates.length === 0) {
1054
+ loadProposalData();
1055
+ }
1056
+ }, [proposalConfig]);
1057
+
1058
+ // Update selected template when data changes and templates are available
1059
+ useEffect(() => {
1060
+ if (data.proposal_template_id && proposalTemplates.length > 0 && !selectedTemplate) {
1061
+ const savedTemplate = proposalTemplates.find(t => t.id == data.proposal_template_id);
1062
+ if (savedTemplate) {
1063
+ console.log('Setting saved template from data update:', savedTemplate);
1064
+ setSelectedTemplate(data.proposal_template_id);
1065
+ }
1066
+ }
1067
+ }, [data.proposal_template_id, proposalTemplates, selectedTemplate]);
1068
+
789
1069
  const handleActionClick = (action) => {
790
1070
  // Handle different actions based on action properties
791
1071
  console.log(`Action ${action.id} clicked:`, action);
@@ -1088,30 +1368,9 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1088
1368
  </div>
1089
1369
  </div>
1090
1370
 
1091
- {/* Proposal Mode Controls (backward compatible) */}
1371
+ {/* Proposal Controls (always active) */}
1092
1372
  {proposalConfig && (
1093
1373
  <div className={styles.proposalControls}>
1094
- <div className={styles.proposalToggle}>
1095
- <button
1096
- className={`${styles.modeToggle} ${proposalMode ? styles.proposalMode : styles.quoteMode}`}
1097
- onClick={toggleProposalMode}
1098
- disabled={isGeneratingProposal}
1099
- >
1100
- {proposalMode ? (
1101
- <>
1102
- <FileText size={16} aria-hidden="true" />
1103
- <span>Proposal Mode</span>
1104
- </>
1105
- ) : (
1106
- <>
1107
- <DollarSign size={16} aria-hidden="true" />
1108
- <span>Quote Mode</span>
1109
- </>
1110
- )}
1111
- </button>
1112
- </div>
1113
-
1114
- {proposalMode && (
1115
1374
  <div className={styles.proposalSettings}>
1116
1375
  <div className={styles.proposalSelectors}>
1117
1376
  <div className={styles.selectorGroup}>
@@ -1119,38 +1378,29 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1119
1378
  <select
1120
1379
  id="templateSelect"
1121
1380
  value={selectedTemplate || ''}
1122
- onChange={(e) => setSelectedTemplate(e.target.value)}
1381
+ onChange={(e) => handleTemplateSelection(e.target.value)}
1123
1382
  className={styles.proposalSelect}
1124
1383
  disabled={isGeneratingProposal}
1125
1384
  >
1126
1385
  <option value="">Select Template</option>
1127
1386
  {proposalTemplates.map((template) => (
1128
1387
  <option key={template.id} value={template.id}>
1129
- {template.name}
1388
+ {template.label}
1130
1389
  </option>
1131
1390
  ))}
1132
1391
  </select>
1133
1392
  </div>
1134
1393
 
1135
- <div className={styles.selectorGroup}>
1136
- <label htmlFor="brandingSelect">Branding:</label>
1137
- <select
1138
- id="brandingSelect"
1139
- value={selectedBranding || ''}
1140
- onChange={(e) => setSelectedBranding(e.target.value)}
1141
- className={styles.proposalSelect}
1394
+ <div className={styles.proposalActions}>
1395
+ <button
1396
+ className={styles.editButton}
1397
+ onClick={openContentEditor}
1142
1398
  disabled={isGeneratingProposal}
1399
+ title="Edit proposal content"
1143
1400
  >
1144
- <option value="">Select Branding</option>
1145
- {brandingProfiles.map((profile) => (
1146
- <option key={profile.id} value={profile.id}>
1147
- {profile.name}
1148
- </option>
1149
- ))}
1150
- </select>
1151
- </div>
1152
-
1153
- <div className={styles.proposalActions}>
1401
+ <Edit3 size={16} aria-hidden="true" />
1402
+ <span>Edit Content</span>
1403
+ </button>
1154
1404
  <button
1155
1405
  className={styles.previewButton}
1156
1406
  onClick={generateProposalPreview}
@@ -1188,12 +1438,11 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1188
1438
  </div>
1189
1439
  </div>
1190
1440
  </div>
1191
- )}
1192
1441
  </div>
1193
1442
  )}
1194
1443
 
1195
1444
  {/* Proposal Preview (if available) */}
1196
- {proposalMode && proposalPreview && (
1445
+ {proposalPreview && (
1197
1446
  <div className={styles.proposalPreviewContainer}>
1198
1447
  <div className={styles.proposalPreviewHeader}>
1199
1448
  <h3>Proposal Preview</h3>
@@ -1212,6 +1461,90 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1212
1461
  </div>
1213
1462
  )}
1214
1463
 
1464
+ {/* Content Editor Modal */}
1465
+ {showContentEditor && (
1466
+ <div className={styles.contentEditorOverlay}>
1467
+ <div className={styles.contentEditorModal}>
1468
+ <div className={styles.contentEditorHeader}>
1469
+ <h3>
1470
+ <FileEdit size={20} />
1471
+ Edit Proposal Content
1472
+ </h3>
1473
+ <button
1474
+ className={styles.closeEditor}
1475
+ onClick={() => setShowContentEditor(false)}
1476
+ aria-label="Close editor"
1477
+ >
1478
+ <X size={18} aria-hidden="true" />
1479
+ </button>
1480
+ </div>
1481
+ <div className={styles.contentEditorBody}>
1482
+ <div className={styles.editorSection}>
1483
+ <label className={styles.editorLabel}>
1484
+ <Settings size={16} />
1485
+ Section Type:
1486
+ <select
1487
+ value={contentSectionType}
1488
+ onChange={(e) => setContentSectionType(e.target.value)}
1489
+ className={styles.sectionTypeSelect}
1490
+ >
1491
+ <option value="content">Content Section</option>
1492
+ <option value="overview">Overview Section</option>
1493
+ <option value="solution">Solution Section</option>
1494
+ <option value="benefits">Benefits Section</option>
1495
+ </select>
1496
+ </label>
1497
+ </div>
1498
+ <div className={styles.editorContainer}>
1499
+ <Editor
1500
+ apiKey="qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc"
1501
+ value={proposalContent}
1502
+ onEditorChange={handleContentChange}
1503
+ init={{
1504
+ height: '100%',
1505
+ min_height: 400,
1506
+ resize: false,
1507
+ menubar: true,
1508
+ plugins: [
1509
+ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
1510
+ 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
1511
+ 'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
1512
+ ],
1513
+ toolbar: 'undo redo | blocks | ' +
1514
+ 'bold italic forecolor | alignleft aligncenter ' +
1515
+ 'alignright alignjustify | bullist numlist outdent indent | ' +
1516
+ 'removeformat | help',
1517
+ content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
1518
+ branding: false
1519
+ }}
1520
+ />
1521
+ </div>
1522
+ </div>
1523
+ <div className={styles.contentEditorFooter}>
1524
+ <div className={styles.footerInfo}>
1525
+ Use rich text formatting to create professional proposal content
1526
+ </div>
1527
+ <div className={styles.footerButtons}>
1528
+ <button
1529
+ className={styles.cancelButton}
1530
+ onClick={() => setShowContentEditor(false)}
1531
+ >
1532
+ <X size={16} />
1533
+ Cancel
1534
+ </button>
1535
+ <button
1536
+ className={styles.saveButton}
1537
+ onClick={saveProposalContent}
1538
+ >
1539
+ <Save size={16} />
1540
+ Save Content
1541
+ </button>
1542
+ </div>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+ )}
1547
+
1215
1548
  {/* Quote content */}
1216
1549
  <div className={styles.quoteContent}>
1217
1550
  {/* Quote header with logo and date */}