@visns-studio/visns-components 5.11.7 → 5.11.9

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';
@@ -19,6 +20,54 @@ import SortableTableBody from './SortableQuoteItems';
19
20
 
20
21
  import styles from './styles/GenericQuote.module.scss';
21
22
 
23
+ // Isolated Proposal Preview Component
24
+ function ProposalPreviewFrame({ content }) {
25
+ const iframeRef = React.useRef(null);
26
+
27
+ React.useEffect(() => {
28
+ if (iframeRef.current && content) {
29
+ const iframe = iframeRef.current;
30
+ const doc = iframe.contentDocument || iframe.contentWindow.document;
31
+
32
+ // Write the complete HTML document to iframe
33
+ doc.open();
34
+ doc.write(content);
35
+ doc.close();
36
+
37
+ // Optional: Add responsive styling to iframe body
38
+ const style = doc.createElement('style');
39
+ style.textContent = `
40
+ body {
41
+ margin: 0;
42
+ padding: 20px;
43
+ font-family: Arial, sans-serif;
44
+ background: white;
45
+ overflow-x: auto;
46
+ }
47
+ * {
48
+ box-sizing: border-box;
49
+ }
50
+ `;
51
+ doc.head.appendChild(style);
52
+ }
53
+ }, [content]);
54
+
55
+ return (
56
+ <iframe
57
+ ref={iframeRef}
58
+ className={styles.proposalPreview}
59
+ title="Proposal Preview"
60
+ sandbox="allow-same-origin"
61
+ style={{
62
+ width: '100%',
63
+ height: '100%',
64
+ border: 'none',
65
+ background: 'white'
66
+ }}
67
+ />
68
+ );
69
+ }
70
+
22
71
  function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
23
72
  const routeParams = useParams();
24
73
  const navigate = useNavigate();
@@ -28,14 +77,16 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
28
77
  const [data, setData] = useState({});
29
78
  const [actions, setActions] = useState(actionItems || []);
30
79
 
31
- // Proposal system state (backward compatible)
32
- const [proposalMode, setProposalMode] = useState(false);
80
+ // Proposal system state (always active)
33
81
  const [proposalTemplates, setProposalTemplates] = useState([]);
34
- const [brandingProfiles, setBrandingProfiles] = useState([]);
35
82
  const [selectedTemplate, setSelectedTemplate] = useState(null);
36
- const [selectedBranding, setSelectedBranding] = useState(null);
37
83
  const [proposalPreview, setProposalPreview] = useState(null);
38
84
  const [isGeneratingProposal, setIsGeneratingProposal] = useState(false);
85
+
86
+ // Content editor state
87
+ const [showContentEditor, setShowContentEditor] = useState(false);
88
+ const [proposalContent, setProposalContent] = useState('');
89
+ const [contentSectionType, setContentSectionType] = useState('content');
39
90
 
40
91
  // Modal state variables
41
92
  const [modalShow, setModalShow] = useState(false);
@@ -504,55 +555,149 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
504
555
  if (!proposalConfig) return;
505
556
 
506
557
  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')
558
+ console.log('Loading proposal templates...');
559
+ // Load proposal templates using dropdown endpoint
560
+ const templatesRes = await CustomFetch(
561
+ '/ajax/proposalTemplates/dropdown',
562
+ 'POST',
563
+ {}
564
+ );
565
+
566
+ console.log('Templates response:', templatesRes);
567
+
568
+ // Handle different response formats
569
+ let templates = [];
570
+ if (templatesRes.data && templatesRes.data.data && Array.isArray(templatesRes.data.data)) {
571
+ templates = templatesRes.data.data;
572
+ } else if (templatesRes.data && Array.isArray(templatesRes.data)) {
573
+ templates = templatesRes.data;
574
+ } else if (Array.isArray(templatesRes)) {
575
+ templates = templatesRes;
576
+ } else if (templatesRes.success && templatesRes.data) {
577
+ templates = templatesRes.data;
578
+ }
579
+
580
+ console.log('Extracted templates:', templates);
581
+
582
+ if (templates && templates.length > 0) {
583
+ setProposalTemplates(templates);
584
+
585
+ // Check if quote already has a saved proposal template
586
+ if (data.proposal_template_id) {
587
+ const savedTemplate = templates.find(t => t.id == data.proposal_template_id);
588
+ if (savedTemplate) {
589
+ console.log('Setting previously saved template:', savedTemplate);
590
+ setSelectedTemplate(data.proposal_template_id);
591
+ } else {
592
+ console.log('Saved template ID not found in available templates:', data.proposal_template_id);
593
+ }
594
+ } else {
595
+ // Set default template if no saved template and available
596
+ const defaultTemplate = templates.find(
597
+ t => t.label?.includes('Standard') || t.label?.includes('Default')
520
598
  );
521
599
  if (defaultTemplate) {
600
+ console.log('Setting default template:', defaultTemplate);
522
601
  setSelectedTemplate(defaultTemplate.id);
523
602
  }
524
603
  }
604
+
605
+ console.log('Templates successfully loaded:', templates.length, 'templates');
606
+ } else {
607
+ console.log('No templates found in response:', templatesRes);
525
608
  }
526
609
 
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);
610
+ // Branding is handled at template creation time, no need to load separately
611
+ } catch (error) {
612
+ console.error('Error loading proposal data:', error);
613
+ toast.error('Failed to load proposal templates');
614
+ }
615
+ };
616
+
617
+ const calculateGrandTotal = () => {
618
+ if (!tables || tables.length === 0) return 0;
619
+
620
+ return tables.reduce((grandTotal, table) => {
621
+ if (!data[table.id] || data[table.id].length === 0) return grandTotal;
622
+
623
+ const tableTotal = table.total
624
+ ? parseFloat(table.total)
625
+ : data[table.id].reduce((sum, row) => {
626
+ let total;
627
+ if (typeof row.total === 'number') {
628
+ total = row.total;
629
+ } else if (row.total) {
630
+ total = parseFloat(
631
+ String(row.total).replace(/[^0-9.-]+/g, '')
632
+ );
633
+ } else if (row.rate && row.qty) {
634
+ const rate = parseFloat(
635
+ String(row.rate).replace(/[^0-9.-]+/g, '')
636
+ );
637
+ const qty = parseFloat(
638
+ String(row.qty).replace(/[^0-9.-]+/g, '')
639
+ );
640
+ total = rate * qty;
641
+ } else {
642
+ total = 0;
545
643
  }
546
- }
644
+ return sum + (isNaN(total) ? 0 : total);
645
+ }, 0);
646
+
647
+ return grandTotal + (isNaN(tableTotal) ? 0 : tableTotal);
648
+ }, 0);
649
+ };
650
+
651
+ const handleTemplateSelection = async (templateId) => {
652
+ setSelectedTemplate(templateId);
653
+
654
+ if (!templateId) return;
655
+
656
+ // Save the selected template to the quote
657
+ try {
658
+ const toastId = toast.loading('Saving template selection...');
659
+
660
+ const updateData = {
661
+ proposal_template_id: templateId
662
+ };
663
+
664
+ const response = await CustomFetch(
665
+ `${page.fetchUrl}/${routeParams[urlParam]}`,
666
+ 'PUT',
667
+ updateData
668
+ );
669
+
670
+ if (response.success || response.data) {
671
+ // Update local data to reflect the change
672
+ setData(prevData => ({
673
+ ...prevData,
674
+ proposal_template_id: templateId
675
+ }));
676
+
677
+ toast.update(toastId, {
678
+ render: 'Template selection saved',
679
+ type: 'success',
680
+ isLoading: false,
681
+ autoClose: 2000,
682
+ closeButton: true,
683
+ });
684
+ } else {
685
+ throw new Error(response.message || 'Failed to save template selection');
547
686
  }
548
687
  } catch (error) {
549
- console.error('Error loading proposal data:', error);
550
- toast.error('Failed to load proposal templates and branding');
688
+ console.error('Error saving template selection:', error);
689
+ toast.error('Failed to save template selection: ' + error.message);
690
+
691
+ // Revert the selection on error
692
+ setSelectedTemplate(data.proposal_template_id || null);
551
693
  }
552
694
  };
553
695
 
554
696
  const generateProposalPreview = async () => {
555
- if (!selectedTemplate || !proposalConfig?.previewEndpoint) return;
697
+ if (!selectedTemplate) {
698
+ toast.error('Please select a template first');
699
+ return;
700
+ }
556
701
 
557
702
  setIsGeneratingProposal(true);
558
703
  const toastId = toast.loading('Generating proposal preview...');
@@ -562,30 +707,106 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
562
707
  proposal_data: {
563
708
  customer_name: data.customer?.name || 'Customer Name',
564
709
  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() :
710
+ `${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
711
  'Customer Address',
567
- quote_number: data.id || 'Q-001',
568
- quote_date: data.created_at || new Date().toISOString().split('T')[0],
712
+ quote_number: String(data.id || 'Q-001'),
713
+ quote_date: data.created_at ? new Date(data.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
569
714
  current_date: new Date().toISOString().split('T')[0],
570
715
  total_amount: formatCurrency(calculateGrandTotal()),
571
716
  company_name: 'VISNS Studio',
717
+ company_address: '660C Newcastle Street, Leederville, WA, 6007',
718
+ company_phone: '08 6383 6600',
719
+ company_email: 'team@omniaglobal.com.au',
720
+ document_title: `Proposal for ${data.customer?.name || 'Customer'}`,
721
+ project_manager: 'VISNS Studio',
572
722
  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 || [],
723
+ // Provide clean item data - just essential fields as simple arrays
724
+ items_onceoff: (data.items_onceoff || []).map(item => ({
725
+ description: String(item.description || ''),
726
+ rate: parseFloat(item.rate || 0),
727
+ qty: parseFloat(item.qty || 1),
728
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
729
+ })),
730
+ items_monthly_subscription: (data.items_monthly_subscription || []).map(item => ({
731
+ description: String(item.description || ''),
732
+ rate: parseFloat(item.rate || 0),
733
+ qty: parseFloat(item.qty || 1),
734
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
735
+ })),
736
+ items_yearly_subscription: (data.items_yearly_subscription || []).map(item => ({
737
+ description: String(item.description || ''),
738
+ rate: parseFloat(item.rate || 0),
739
+ qty: parseFloat(item.qty || 1),
740
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
741
+ })),
742
+ // Include saved proposal content
743
+ proposal_content: data.proposal_content || null,
744
+ proposal_section_type: data.proposal_section_type || 'content',
576
745
  },
577
- template_id: selectedTemplate,
578
- branding_id: selectedBranding,
746
+ template_id: parseInt(selectedTemplate),
747
+ download: false, // For preview
748
+ paper: 'a4',
749
+ orientation: 'portrait'
579
750
  };
580
751
 
752
+ console.log('Sending proposal data for preview:', JSON.stringify(proposalData, null, 2));
753
+
581
754
  const response = await CustomFetch(
582
- proposalConfig.previewEndpoint,
755
+ '/ajax/pdf/preview-proposal',
583
756
  'POST',
584
757
  proposalData
585
758
  );
586
759
 
587
- if (response.success) {
588
- setProposalPreview(response.html);
760
+ console.log('Preview response:', response);
761
+
762
+ if (response.data && response.data.success) {
763
+ // Add DomPDF-compatible styling to the preview HTML to match PDF output
764
+ let htmlContent = response.data.html;
765
+
766
+ // Inject CSS constants for consistent styling
767
+ const cssInjection = `
768
+ <style>
769
+ .omnia-cover-page {
770
+ background-color: #059669 !important;
771
+ color: white !important;
772
+ }
773
+ table {
774
+ border: 2px solid #333333 !important;
775
+ border-collapse: collapse !important;
776
+ }
777
+ table th {
778
+ background-color: #4fbfa5 !important;
779
+ color: white !important;
780
+ border: 1px solid #333333 !important;
781
+ }
782
+ table td {
783
+ border: 1px solid #333333 !important;
784
+ font-weight: 500 !important;
785
+ }
786
+ table tr:nth-child(even) td {
787
+ background-color: #f9f9f9 !important;
788
+ }
789
+ table tfoot tr {
790
+ background-color: #f3f4f6 !important;
791
+ }
792
+ table tfoot tr:last-child {
793
+ background-color: #333333 !important;
794
+ }
795
+ table tfoot tr:last-child td {
796
+ color: white !important;
797
+ font-weight: bold !important;
798
+ }
799
+ </style>
800
+ `;
801
+
802
+ // Insert CSS after the <head> tag or before closing </head>
803
+ if (htmlContent.includes('</head>')) {
804
+ htmlContent = htmlContent.replace('</head>', cssInjection + '</head>');
805
+ } else {
806
+ htmlContent = cssInjection + htmlContent;
807
+ }
808
+
809
+ setProposalPreview(htmlContent);
589
810
  toast.update(toastId, {
590
811
  render: 'Proposal preview generated successfully',
591
812
  type: 'success',
@@ -594,7 +815,8 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
594
815
  closeButton: true,
595
816
  });
596
817
  } else {
597
- throw new Error(response.message || 'Failed to generate preview');
818
+ console.error('Preview failed with response:', response);
819
+ throw new Error(response.data?.message || response.data?.error || 'Failed to generate preview');
598
820
  }
599
821
  } catch (error) {
600
822
  console.error('Error generating proposal preview:', error);
@@ -611,7 +833,10 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
611
833
  };
612
834
 
613
835
  const generateProposalPDF = async () => {
614
- if (!selectedTemplate || !proposalConfig?.pdfEndpoint) return;
836
+ if (!selectedTemplate) {
837
+ toast.error('Please select a template first');
838
+ return;
839
+ }
615
840
 
616
841
  setIsGeneratingProposal(true);
617
842
  const toastId = toast.loading('Generating proposal PDF...');
@@ -621,28 +846,53 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
621
846
  proposal_data: {
622
847
  customer_name: data.customer?.name || 'Customer Name',
623
848
  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() :
849
+ `${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
850
  'Customer Address',
626
- quote_number: data.id || 'Q-001',
627
- quote_date: data.created_at || new Date().toISOString().split('T')[0],
851
+ quote_number: String(data.id || 'Q-001'),
852
+ quote_date: data.created_at ? new Date(data.created_at).toISOString().split('T')[0] : new Date().toISOString().split('T')[0],
628
853
  current_date: new Date().toISOString().split('T')[0],
629
854
  total_amount: formatCurrency(calculateGrandTotal()),
630
855
  company_name: 'VISNS Studio',
856
+ company_address: '660C Newcastle Street, Leederville, WA, 6007',
857
+ company_phone: '08 6383 6600',
858
+ company_email: 'team@omniaglobal.com.au',
859
+ document_title: `Proposal for ${data.customer?.name || 'Customer'}`,
860
+ project_manager: 'VISNS Studio',
631
861
  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 || [],
862
+ // Provide clean item data - just essential fields as simple arrays
863
+ items_onceoff: (data.items_onceoff || []).map(item => ({
864
+ description: String(item.description || ''),
865
+ rate: parseFloat(item.rate || 0),
866
+ qty: parseFloat(item.qty || 1),
867
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
868
+ })),
869
+ items_monthly_subscription: (data.items_monthly_subscription || []).map(item => ({
870
+ description: String(item.description || ''),
871
+ rate: parseFloat(item.rate || 0),
872
+ qty: parseFloat(item.qty || 1),
873
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
874
+ })),
875
+ items_yearly_subscription: (data.items_yearly_subscription || []).map(item => ({
876
+ description: String(item.description || ''),
877
+ rate: parseFloat(item.rate || 0),
878
+ qty: parseFloat(item.qty || 1),
879
+ total: parseFloat((parseFloat(item.rate || 0) * parseFloat(item.qty || 1)).toFixed(2))
880
+ })),
881
+ // Include saved proposal content
882
+ proposal_content: data.proposal_content || null,
883
+ proposal_section_type: data.proposal_section_type || 'content',
635
884
  },
636
- template_id: selectedTemplate,
637
- branding_id: selectedBranding,
638
- filename: `proposal-${data.id || 'draft'}-${new Date().toISOString().split('T')[0]}.pdf`,
885
+ template_id: parseInt(selectedTemplate),
886
+ filename: `${(data.customer?.name || 'Customer').replace(/[/\\?%*:|"<>]/g, '-')} - Proposal #${data.id || 'draft'} - ${(data.label || 'Quote').replace(/[/\\?%*:|"<>]/g, '-')}.pdf`,
639
887
  download: true,
888
+ paper: 'a4',
889
+ orientation: 'portrait'
640
890
  };
641
891
 
642
892
  // Create a form for file download
643
893
  const form = document.createElement('form');
644
894
  form.method = 'POST';
645
- form.action = proposalConfig.pdfEndpoint;
895
+ form.action = `/ajax/quotes/${data.id}/generate-proposal`;
646
896
  form.style.display = 'none';
647
897
 
648
898
  // Add CSRF token
@@ -668,12 +918,6 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
668
918
  templateInput.value = selectedTemplate;
669
919
  form.appendChild(templateInput);
670
920
 
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
921
  const filenameInput = document.createElement('input');
678
922
  filenameInput.type = 'hidden';
679
923
  filenameInput.name = 'filename';
@@ -706,15 +950,64 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
706
950
  }
707
951
  };
708
952
 
709
- const toggleProposalMode = () => {
710
- const newMode = !proposalMode;
711
- setProposalMode(newMode);
712
-
713
- if (newMode && proposalTemplates.length === 0) {
714
- loadProposalData();
953
+
954
+ const openContentEditor = () => {
955
+ // Load existing content if available
956
+ if (data.proposal_content) {
957
+ setProposalContent(data.proposal_content);
958
+ } else {
959
+ // Set default content template
960
+ setProposalContent(`
961
+ <h2>Project Overview</h2>
962
+ <p>Describe the project background and objectives here...</p>
963
+
964
+ <h2>Proposed Solution</h2>
965
+ <p>Detail your proposed solution and approach...</p>
966
+
967
+ <h2>Benefits</h2>
968
+ <ul>
969
+ <li>Key benefit 1</li>
970
+ <li>Key benefit 2</li>
971
+ <li>Key benefit 3</li>
972
+ </ul>
973
+
974
+ <h2>Implementation Timeline</h2>
975
+ <p>Outline the implementation phases and timeline...</p>
976
+ `.trim());
715
977
  }
716
-
717
- toast.info(newMode ? 'Switched to Proposal Mode' : 'Switched to Quote Mode');
978
+ setShowContentEditor(true);
979
+ };
980
+
981
+ const saveProposalContent = async () => {
982
+ try {
983
+ const response = await CustomFetch(
984
+ `/ajax/quotes/${data.id}/proposal-content`,
985
+ 'POST',
986
+ {
987
+ content: proposalContent,
988
+ section_type: contentSectionType
989
+ }
990
+ );
991
+
992
+ if (response.data && response.data.success) {
993
+ toast.success('Proposal content saved successfully');
994
+ setShowContentEditor(false);
995
+ // Update the data object with the new content
996
+ setData(prev => ({
997
+ ...prev,
998
+ proposal_content: proposalContent
999
+ }));
1000
+ } else {
1001
+ throw new Error(response.data?.message || 'Failed to save content');
1002
+ }
1003
+ } catch (error) {
1004
+ console.error('Error saving proposal content:', error);
1005
+ toast.error('Failed to save proposal content: ' + error.message);
1006
+ }
1007
+ };
1008
+
1009
+ const handleContentChange = (content) => {
1010
+ setProposalContent(content);
718
1011
  };
719
1012
 
720
1013
  const handleAddItem = (tableId) => {
@@ -763,9 +1056,21 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
763
1056
 
764
1057
  useEffect(() => {
765
1058
  if (actionItems && actionItems.length > 0) {
766
- setActions(actionItems);
1059
+ // Filter out proposal-related actions when proposal controls are active
1060
+ const filteredActions = proposalConfig
1061
+ ? actionItems.filter(action => {
1062
+ // Remove actions that are now handled in the proposal controls section
1063
+ const proposalActionIds = ['generateProposalPDF', 'previewProposal'];
1064
+ const proposalActionLabels = ['Generate Proposal PDF', 'Preview Proposal'];
1065
+
1066
+ return !proposalActionIds.includes(action.id) &&
1067
+ !proposalActionLabels.includes(action.label);
1068
+ })
1069
+ : actionItems;
1070
+
1071
+ setActions(filteredActions);
767
1072
  }
768
- }, [actionItems]);
1073
+ }, [actionItems, proposalConfig]);
769
1074
 
770
1075
  useEffect(() => {
771
1076
  const fetchData = async () => {
@@ -777,6 +1082,11 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
777
1082
  {}
778
1083
  );
779
1084
  setData(res.data); // Assuming res is the data to set
1085
+
1086
+ // If quote has a saved proposal template, set it as selected
1087
+ if (res.data && res.data.proposal_template_id) {
1088
+ setSelectedTemplate(res.data.proposal_template_id);
1089
+ }
780
1090
  } catch (error) {
781
1091
  console.error('Error fetching data:', error);
782
1092
  }
@@ -786,6 +1096,24 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
786
1096
  fetchData();
787
1097
  }, [routeParams['*']]);
788
1098
 
1099
+ // Load proposal data on component mount
1100
+ useEffect(() => {
1101
+ if (proposalConfig && proposalTemplates.length === 0) {
1102
+ loadProposalData();
1103
+ }
1104
+ }, [proposalConfig]);
1105
+
1106
+ // Update selected template when data changes and templates are available
1107
+ useEffect(() => {
1108
+ if (data.proposal_template_id && proposalTemplates.length > 0 && !selectedTemplate) {
1109
+ const savedTemplate = proposalTemplates.find(t => t.id == data.proposal_template_id);
1110
+ if (savedTemplate) {
1111
+ console.log('Setting saved template from data update:', savedTemplate);
1112
+ setSelectedTemplate(data.proposal_template_id);
1113
+ }
1114
+ }
1115
+ }, [data.proposal_template_id, proposalTemplates, selectedTemplate]);
1116
+
789
1117
  const handleActionClick = (action) => {
790
1118
  // Handle different actions based on action properties
791
1119
  console.log(`Action ${action.id} clicked:`, action);
@@ -1088,30 +1416,9 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1088
1416
  </div>
1089
1417
  </div>
1090
1418
 
1091
- {/* Proposal Mode Controls (backward compatible) */}
1419
+ {/* Proposal Controls (always active) */}
1092
1420
  {proposalConfig && (
1093
1421
  <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
1422
  <div className={styles.proposalSettings}>
1116
1423
  <div className={styles.proposalSelectors}>
1117
1424
  <div className={styles.selectorGroup}>
@@ -1119,38 +1426,29 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1119
1426
  <select
1120
1427
  id="templateSelect"
1121
1428
  value={selectedTemplate || ''}
1122
- onChange={(e) => setSelectedTemplate(e.target.value)}
1429
+ onChange={(e) => handleTemplateSelection(e.target.value)}
1123
1430
  className={styles.proposalSelect}
1124
1431
  disabled={isGeneratingProposal}
1125
1432
  >
1126
1433
  <option value="">Select Template</option>
1127
1434
  {proposalTemplates.map((template) => (
1128
1435
  <option key={template.id} value={template.id}>
1129
- {template.name}
1436
+ {template.label}
1130
1437
  </option>
1131
1438
  ))}
1132
1439
  </select>
1133
1440
  </div>
1134
1441
 
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}
1442
+ <div className={styles.proposalActions}>
1443
+ <button
1444
+ className={styles.editButton}
1445
+ onClick={openContentEditor}
1142
1446
  disabled={isGeneratingProposal}
1447
+ title="Edit proposal content"
1143
1448
  >
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}>
1449
+ <Edit3 size={16} aria-hidden="true" />
1450
+ <span>Edit Content</span>
1451
+ </button>
1154
1452
  <button
1155
1453
  className={styles.previewButton}
1156
1454
  onClick={generateProposalPreview}
@@ -1188,12 +1486,11 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1188
1486
  </div>
1189
1487
  </div>
1190
1488
  </div>
1191
- )}
1192
1489
  </div>
1193
1490
  )}
1194
1491
 
1195
1492
  {/* Proposal Preview (if available) */}
1196
- {proposalMode && proposalPreview && (
1493
+ {proposalPreview && (
1197
1494
  <div className={styles.proposalPreviewContainer}>
1198
1495
  <div className={styles.proposalPreviewHeader}>
1199
1496
  <h3>Proposal Preview</h3>
@@ -1205,10 +1502,91 @@ function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
1205
1502
  <X size={16} aria-hidden="true" />
1206
1503
  </button>
1207
1504
  </div>
1208
- <div
1209
- className={styles.proposalPreview}
1210
- dangerouslySetInnerHTML={{ __html: proposalPreview }}
1211
- />
1505
+ <ProposalPreviewFrame content={proposalPreview} />
1506
+ </div>
1507
+ )}
1508
+
1509
+ {/* Content Editor Modal */}
1510
+ {showContentEditor && (
1511
+ <div className={styles.contentEditorOverlay}>
1512
+ <div className={styles.contentEditorModal}>
1513
+ <div className={styles.contentEditorHeader}>
1514
+ <h3>
1515
+ <FileEdit size={20} />
1516
+ Edit Proposal Content
1517
+ </h3>
1518
+ <button
1519
+ className={styles.closeEditor}
1520
+ onClick={() => setShowContentEditor(false)}
1521
+ aria-label="Close editor"
1522
+ >
1523
+ <X size={18} aria-hidden="true" />
1524
+ </button>
1525
+ </div>
1526
+ <div className={styles.contentEditorBody}>
1527
+ <div className={styles.editorSection}>
1528
+ <label className={styles.editorLabel}>
1529
+ <Settings size={16} />
1530
+ Section Type:
1531
+ <select
1532
+ value={contentSectionType}
1533
+ onChange={(e) => setContentSectionType(e.target.value)}
1534
+ className={styles.sectionTypeSelect}
1535
+ >
1536
+ <option value="content">Content Section</option>
1537
+ <option value="overview">Overview Section</option>
1538
+ <option value="solution">Solution Section</option>
1539
+ <option value="benefits">Benefits Section</option>
1540
+ </select>
1541
+ </label>
1542
+ </div>
1543
+ <div className={styles.editorContainer}>
1544
+ <Editor
1545
+ apiKey="qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc"
1546
+ value={proposalContent}
1547
+ onEditorChange={handleContentChange}
1548
+ init={{
1549
+ height: '100%',
1550
+ min_height: 400,
1551
+ resize: false,
1552
+ menubar: true,
1553
+ plugins: [
1554
+ 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
1555
+ 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
1556
+ 'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
1557
+ ],
1558
+ toolbar: 'undo redo | blocks | ' +
1559
+ 'bold italic forecolor | alignleft aligncenter ' +
1560
+ 'alignright alignjustify | bullist numlist outdent indent | ' +
1561
+ 'removeformat | help',
1562
+ content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
1563
+ branding: false
1564
+ }}
1565
+ />
1566
+ </div>
1567
+ </div>
1568
+ <div className={styles.contentEditorFooter}>
1569
+ <div className={styles.footerInfo}>
1570
+ Use rich text formatting to create professional proposal content
1571
+ </div>
1572
+ <div className={styles.footerButtons}>
1573
+ <button
1574
+ className={styles.cancelButton}
1575
+ onClick={() => setShowContentEditor(false)}
1576
+ >
1577
+ <X size={16} />
1578
+ Cancel
1579
+ </button>
1580
+ <button
1581
+ className={styles.saveButton}
1582
+ onClick={saveProposalContent}
1583
+ >
1584
+ <Save size={16} />
1585
+ Save Content
1586
+ </button>
1587
+ </div>
1588
+ </div>
1589
+ </div>
1212
1590
  </div>
1213
1591
  )}
1214
1592