@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.
- package/package.json +1 -1
- package/src/components/crm/Field.jsx +209 -82
- package/src/components/crm/generic/GenericQuote.jsx +454 -121
- package/src/components/crm/generic/styles/GenericQuote.module.scss +580 -40
- package/src/components/crm/styles/Field.module.scss +301 -0
- package/src/components/proposal/ProposalTemplatePreview.css +92 -0
- package/src/components/proposal/ProposalTemplatePreview.jsx +7 -1
- package/src/components/proposal/ProposalTemplateSectionManager.css +69 -10
- package/src/components/proposal/ProposalTemplateSectionManager.jsx +33 -7
- package/src/components/proposal/SectionEditor.css +62 -7
- package/src/components/proposal/SectionEditor.jsx +48 -14
- package/src/components/proposal/SectionTypeSelector.css +54 -0
- package/src/components/proposal/SectionTypeSelector.jsx +15 -3
|
@@ -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 (
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
550
|
-
toast.error('Failed to
|
|
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
|
|
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
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
+
'/ajax/pdf/preview-proposal',
|
|
583
708
|
'POST',
|
|
584
709
|
proposalData
|
|
585
710
|
);
|
|
586
711
|
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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 =
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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.
|
|
1388
|
+
{template.label}
|
|
1130
1389
|
</option>
|
|
1131
1390
|
))}
|
|
1132
1391
|
</select>
|
|
1133
1392
|
</div>
|
|
1134
1393
|
|
|
1135
|
-
<div className={styles.
|
|
1136
|
-
<
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
<
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
{
|
|
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 */}
|