@visns-studio/visns-components 5.11.5 → 5.11.7
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/README.md +353 -0
- package/package.json +5 -5
- package/src/components/crm/Field.jsx +88 -60
- package/src/components/crm/columns/ColumnRenderers.jsx +19 -0
- package/src/components/crm/generic/GenericDetail.jsx +40 -0
- package/src/components/crm/generic/GenericEditableTable.jsx +3 -3
- package/src/components/crm/generic/GenericQuote.jsx +353 -1
- package/src/components/crm/generic/styles/GenericEditableTable.module.scss +102 -22
- package/src/components/crm/generic/styles/GenericQuote.module.scss +240 -0
- package/src/components/proposal/ProposalTemplatePreview.css +438 -0
- package/src/components/proposal/ProposalTemplatePreview.jsx +337 -0
- package/src/components/proposal/ProposalTemplateSectionManager.css +338 -0
- package/src/components/proposal/ProposalTemplateSectionManager.jsx +473 -0
- package/src/components/proposal/SectionEditor.css +563 -0
- package/src/components/proposal/SectionEditor.jsx +412 -0
- package/src/components/proposal/SectionTypeSelector.css +277 -0
- package/src/components/proposal/SectionTypeSelector.jsx +158 -0
- package/src/components/proposal/VariableInserter.css +392 -0
- package/src/components/proposal/VariableInserter.jsx +260 -0
- package/src/components/proposal/index.js +5 -0
- package/src/components/styles/global.css +179 -1
- package/src/index.js +13 -0
- package/src/utils/urlBuilder.js +40 -0
|
@@ -74,6 +74,10 @@ import QrCode from '../QrCode';
|
|
|
74
74
|
import Table from '../DataGrid';
|
|
75
75
|
import TableFilter from '../TableFilter';
|
|
76
76
|
|
|
77
|
+
// Proposal components
|
|
78
|
+
import ProposalTemplateSectionManager from '../../proposal/ProposalTemplateSectionManager';
|
|
79
|
+
import ProposalTemplatePreview from '../../proposal/ProposalTemplatePreview';
|
|
80
|
+
|
|
77
81
|
import styles from './styles/GenericDetail.module.scss';
|
|
78
82
|
|
|
79
83
|
const localizer = momentLocalizer(moment);
|
|
@@ -2514,7 +2518,43 @@ function GenericDetail({
|
|
|
2514
2518
|
/>
|
|
2515
2519
|
);
|
|
2516
2520
|
}
|
|
2521
|
+
if (activeTabConfig.component === 'ProposalTemplateSectionManager') {
|
|
2522
|
+
return (
|
|
2523
|
+
<ProposalTemplateSectionManager
|
|
2524
|
+
{...activeTabConfig.props}
|
|
2525
|
+
templateId={routeParams[urlParam]}
|
|
2526
|
+
onUpdate={handleReload}
|
|
2527
|
+
/>
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
if (activeTabConfig.component === 'ProposalTemplatePreview') {
|
|
2531
|
+
return (
|
|
2532
|
+
<ProposalTemplatePreview
|
|
2533
|
+
{...activeTabConfig.props}
|
|
2534
|
+
templateId={routeParams[urlParam]}
|
|
2535
|
+
onUpdate={handleReload}
|
|
2536
|
+
/>
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2517
2539
|
return null;
|
|
2540
|
+
case 'proposalSectionManager':
|
|
2541
|
+
return (
|
|
2542
|
+
<ProposalTemplateSectionManager
|
|
2543
|
+
{...activeTabConfig.props}
|
|
2544
|
+
templateId={routeParams[urlParam]}
|
|
2545
|
+
templateData={data}
|
|
2546
|
+
onUpdate={handleReload}
|
|
2547
|
+
/>
|
|
2548
|
+
);
|
|
2549
|
+
case 'proposalPreview':
|
|
2550
|
+
return (
|
|
2551
|
+
<ProposalTemplatePreview
|
|
2552
|
+
{...activeTabConfig.props}
|
|
2553
|
+
templateId={routeParams[urlParam]}
|
|
2554
|
+
templateData={data}
|
|
2555
|
+
onUpdate={handleReload}
|
|
2556
|
+
/>
|
|
2557
|
+
);
|
|
2518
2558
|
default:
|
|
2519
2559
|
return null;
|
|
2520
2560
|
}
|
|
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
|
|
|
3
3
|
import debounce from 'lodash.debounce';
|
|
4
4
|
import { toast } from 'react-toastify';
|
|
5
5
|
import { Trash2, ArrowUp, ArrowDown, Copy, Droplets } from 'lucide-react';
|
|
6
|
-
import {
|
|
6
|
+
import { Compact } from '@uiw/react-color';
|
|
7
7
|
import { confirmDialog } from '../../utils/ConfirmDialog';
|
|
8
8
|
|
|
9
9
|
import CustomFetch from '../Fetch';
|
|
@@ -85,10 +85,10 @@ const ColourPicker = ({ value, columnId, keyCounter, onChange }) => {
|
|
|
85
85
|
onClick={() => setColorPickerShow(false)}
|
|
86
86
|
/>
|
|
87
87
|
<div className={styles.pickerContainer}>
|
|
88
|
-
<
|
|
88
|
+
<Compact
|
|
89
89
|
color={value || ''}
|
|
90
90
|
colors={pastelColors}
|
|
91
|
-
|
|
91
|
+
onChange={(color) => {
|
|
92
92
|
onChange(
|
|
93
93
|
color.hex,
|
|
94
94
|
columnId,
|
|
@@ -9,6 +9,7 @@ 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
13
|
|
|
13
14
|
import CustomFetch from '../../crm/Fetch';
|
|
14
15
|
import ActionButtons from './ActionButtons';
|
|
@@ -18,7 +19,7 @@ import SortableTableBody from './SortableQuoteItems';
|
|
|
18
19
|
|
|
19
20
|
import styles from './styles/GenericQuote.module.scss';
|
|
20
21
|
|
|
21
|
-
function GenericQuote({ logo, setting, urlParam }) {
|
|
22
|
+
function GenericQuote({ logo, setting, urlParam, proposalConfig = null }) {
|
|
22
23
|
const routeParams = useParams();
|
|
23
24
|
const navigate = useNavigate();
|
|
24
25
|
|
|
@@ -26,6 +27,15 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
26
27
|
|
|
27
28
|
const [data, setData] = useState({});
|
|
28
29
|
const [actions, setActions] = useState(actionItems || []);
|
|
30
|
+
|
|
31
|
+
// Proposal system state (backward compatible)
|
|
32
|
+
const [proposalMode, setProposalMode] = useState(false);
|
|
33
|
+
const [proposalTemplates, setProposalTemplates] = useState([]);
|
|
34
|
+
const [brandingProfiles, setBrandingProfiles] = useState([]);
|
|
35
|
+
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
|
36
|
+
const [selectedBranding, setSelectedBranding] = useState(null);
|
|
37
|
+
const [proposalPreview, setProposalPreview] = useState(null);
|
|
38
|
+
const [isGeneratingProposal, setIsGeneratingProposal] = useState(false);
|
|
29
39
|
|
|
30
40
|
// Modal state variables
|
|
31
41
|
const [modalShow, setModalShow] = useState(false);
|
|
@@ -489,6 +499,224 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
489
499
|
});
|
|
490
500
|
};
|
|
491
501
|
|
|
502
|
+
// Proposal system functions (backward compatible)
|
|
503
|
+
const loadProposalData = async () => {
|
|
504
|
+
if (!proposalConfig) return;
|
|
505
|
+
|
|
506
|
+
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')
|
|
520
|
+
);
|
|
521
|
+
if (defaultTemplate) {
|
|
522
|
+
setSelectedTemplate(defaultTemplate.id);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
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);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error('Error loading proposal data:', error);
|
|
550
|
+
toast.error('Failed to load proposal templates and branding');
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const generateProposalPreview = async () => {
|
|
555
|
+
if (!selectedTemplate || !proposalConfig?.previewEndpoint) return;
|
|
556
|
+
|
|
557
|
+
setIsGeneratingProposal(true);
|
|
558
|
+
const toastId = toast.loading('Generating proposal preview...');
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const proposalData = {
|
|
562
|
+
proposal_data: {
|
|
563
|
+
customer_name: data.customer?.name || 'Customer Name',
|
|
564
|
+
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() :
|
|
566
|
+
'Customer Address',
|
|
567
|
+
quote_number: data.id || 'Q-001',
|
|
568
|
+
quote_date: data.created_at || new Date().toISOString().split('T')[0],
|
|
569
|
+
current_date: new Date().toISOString().split('T')[0],
|
|
570
|
+
total_amount: formatCurrency(calculateGrandTotal()),
|
|
571
|
+
company_name: 'VISNS Studio',
|
|
572
|
+
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 || [],
|
|
576
|
+
},
|
|
577
|
+
template_id: selectedTemplate,
|
|
578
|
+
branding_id: selectedBranding,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const response = await CustomFetch(
|
|
582
|
+
proposalConfig.previewEndpoint,
|
|
583
|
+
'POST',
|
|
584
|
+
proposalData
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
if (response.success) {
|
|
588
|
+
setProposalPreview(response.html);
|
|
589
|
+
toast.update(toastId, {
|
|
590
|
+
render: 'Proposal preview generated successfully',
|
|
591
|
+
type: 'success',
|
|
592
|
+
isLoading: false,
|
|
593
|
+
autoClose: 3000,
|
|
594
|
+
closeButton: true,
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
throw new Error(response.message || 'Failed to generate preview');
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {
|
|
600
|
+
console.error('Error generating proposal preview:', error);
|
|
601
|
+
toast.update(toastId, {
|
|
602
|
+
render: 'Failed to generate proposal preview: ' + error.message,
|
|
603
|
+
type: 'error',
|
|
604
|
+
isLoading: false,
|
|
605
|
+
autoClose: 5000,
|
|
606
|
+
closeButton: true,
|
|
607
|
+
});
|
|
608
|
+
} finally {
|
|
609
|
+
setIsGeneratingProposal(false);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const generateProposalPDF = async () => {
|
|
614
|
+
if (!selectedTemplate || !proposalConfig?.pdfEndpoint) return;
|
|
615
|
+
|
|
616
|
+
setIsGeneratingProposal(true);
|
|
617
|
+
const toastId = toast.loading('Generating proposal PDF...');
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const proposalData = {
|
|
621
|
+
proposal_data: {
|
|
622
|
+
customer_name: data.customer?.name || 'Customer Name',
|
|
623
|
+
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() :
|
|
625
|
+
'Customer Address',
|
|
626
|
+
quote_number: data.id || 'Q-001',
|
|
627
|
+
quote_date: data.created_at || new Date().toISOString().split('T')[0],
|
|
628
|
+
current_date: new Date().toISOString().split('T')[0],
|
|
629
|
+
total_amount: formatCurrency(calculateGrandTotal()),
|
|
630
|
+
company_name: 'VISNS Studio',
|
|
631
|
+
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 || [],
|
|
635
|
+
},
|
|
636
|
+
template_id: selectedTemplate,
|
|
637
|
+
branding_id: selectedBranding,
|
|
638
|
+
filename: `proposal-${data.id || 'draft'}-${new Date().toISOString().split('T')[0]}.pdf`,
|
|
639
|
+
download: true,
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// Create a form for file download
|
|
643
|
+
const form = document.createElement('form');
|
|
644
|
+
form.method = 'POST';
|
|
645
|
+
form.action = proposalConfig.pdfEndpoint;
|
|
646
|
+
form.style.display = 'none';
|
|
647
|
+
|
|
648
|
+
// Add CSRF token
|
|
649
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
|
650
|
+
if (csrfToken) {
|
|
651
|
+
const csrfInput = document.createElement('input');
|
|
652
|
+
csrfInput.type = 'hidden';
|
|
653
|
+
csrfInput.name = '_token';
|
|
654
|
+
csrfInput.value = csrfToken;
|
|
655
|
+
form.appendChild(csrfInput);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Add proposal data as hidden input
|
|
659
|
+
const dataInput = document.createElement('input');
|
|
660
|
+
dataInput.type = 'hidden';
|
|
661
|
+
dataInput.name = 'proposal_data';
|
|
662
|
+
dataInput.value = JSON.stringify(proposalData.proposal_data);
|
|
663
|
+
form.appendChild(dataInput);
|
|
664
|
+
|
|
665
|
+
const templateInput = document.createElement('input');
|
|
666
|
+
templateInput.type = 'hidden';
|
|
667
|
+
templateInput.name = 'template_id';
|
|
668
|
+
templateInput.value = selectedTemplate;
|
|
669
|
+
form.appendChild(templateInput);
|
|
670
|
+
|
|
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
|
+
const filenameInput = document.createElement('input');
|
|
678
|
+
filenameInput.type = 'hidden';
|
|
679
|
+
filenameInput.name = 'filename';
|
|
680
|
+
filenameInput.value = proposalData.filename;
|
|
681
|
+
form.appendChild(filenameInput);
|
|
682
|
+
|
|
683
|
+
// Submit form to trigger download
|
|
684
|
+
document.body.appendChild(form);
|
|
685
|
+
form.submit();
|
|
686
|
+
document.body.removeChild(form);
|
|
687
|
+
|
|
688
|
+
toast.update(toastId, {
|
|
689
|
+
render: 'Proposal PDF download started',
|
|
690
|
+
type: 'success',
|
|
691
|
+
isLoading: false,
|
|
692
|
+
autoClose: 3000,
|
|
693
|
+
closeButton: true,
|
|
694
|
+
});
|
|
695
|
+
} catch (error) {
|
|
696
|
+
console.error('Error generating proposal PDF:', error);
|
|
697
|
+
toast.update(toastId, {
|
|
698
|
+
render: 'Failed to generate proposal PDF: ' + error.message,
|
|
699
|
+
type: 'error',
|
|
700
|
+
isLoading: false,
|
|
701
|
+
autoClose: 5000,
|
|
702
|
+
closeButton: true,
|
|
703
|
+
});
|
|
704
|
+
} finally {
|
|
705
|
+
setIsGeneratingProposal(false);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const toggleProposalMode = () => {
|
|
710
|
+
const newMode = !proposalMode;
|
|
711
|
+
setProposalMode(newMode);
|
|
712
|
+
|
|
713
|
+
if (newMode && proposalTemplates.length === 0) {
|
|
714
|
+
loadProposalData();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
toast.info(newMode ? 'Switched to Proposal Mode' : 'Switched to Quote Mode');
|
|
718
|
+
};
|
|
719
|
+
|
|
492
720
|
const handleAddItem = (tableId) => {
|
|
493
721
|
console.log('Add item to table:', tableId);
|
|
494
722
|
|
|
@@ -860,6 +1088,130 @@ function GenericQuote({ logo, setting, urlParam }) {
|
|
|
860
1088
|
</div>
|
|
861
1089
|
</div>
|
|
862
1090
|
|
|
1091
|
+
{/* Proposal Mode Controls (backward compatible) */}
|
|
1092
|
+
{proposalConfig && (
|
|
1093
|
+
<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
|
+
<div className={styles.proposalSettings}>
|
|
1116
|
+
<div className={styles.proposalSelectors}>
|
|
1117
|
+
<div className={styles.selectorGroup}>
|
|
1118
|
+
<label htmlFor="templateSelect">Template:</label>
|
|
1119
|
+
<select
|
|
1120
|
+
id="templateSelect"
|
|
1121
|
+
value={selectedTemplate || ''}
|
|
1122
|
+
onChange={(e) => setSelectedTemplate(e.target.value)}
|
|
1123
|
+
className={styles.proposalSelect}
|
|
1124
|
+
disabled={isGeneratingProposal}
|
|
1125
|
+
>
|
|
1126
|
+
<option value="">Select Template</option>
|
|
1127
|
+
{proposalTemplates.map((template) => (
|
|
1128
|
+
<option key={template.id} value={template.id}>
|
|
1129
|
+
{template.name}
|
|
1130
|
+
</option>
|
|
1131
|
+
))}
|
|
1132
|
+
</select>
|
|
1133
|
+
</div>
|
|
1134
|
+
|
|
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}
|
|
1142
|
+
disabled={isGeneratingProposal}
|
|
1143
|
+
>
|
|
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}>
|
|
1154
|
+
<button
|
|
1155
|
+
className={styles.previewButton}
|
|
1156
|
+
onClick={generateProposalPreview}
|
|
1157
|
+
disabled={!selectedTemplate || isGeneratingProposal}
|
|
1158
|
+
>
|
|
1159
|
+
{isGeneratingProposal ? (
|
|
1160
|
+
<>
|
|
1161
|
+
<Clock size={16} aria-hidden="true" />
|
|
1162
|
+
<span>Generating...</span>
|
|
1163
|
+
</>
|
|
1164
|
+
) : (
|
|
1165
|
+
<>
|
|
1166
|
+
<Eye size={16} aria-hidden="true" />
|
|
1167
|
+
<span>Preview</span>
|
|
1168
|
+
</>
|
|
1169
|
+
)}
|
|
1170
|
+
</button>
|
|
1171
|
+
<button
|
|
1172
|
+
className={styles.pdfButton}
|
|
1173
|
+
onClick={generateProposalPDF}
|
|
1174
|
+
disabled={!selectedTemplate || isGeneratingProposal}
|
|
1175
|
+
>
|
|
1176
|
+
{isGeneratingProposal ? (
|
|
1177
|
+
<>
|
|
1178
|
+
<Clock size={16} aria-hidden="true" />
|
|
1179
|
+
<span>Generating...</span>
|
|
1180
|
+
</>
|
|
1181
|
+
) : (
|
|
1182
|
+
<>
|
|
1183
|
+
<FileText size={16} aria-hidden="true" />
|
|
1184
|
+
<span>Generate PDF</span>
|
|
1185
|
+
</>
|
|
1186
|
+
)}
|
|
1187
|
+
</button>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
</div>
|
|
1191
|
+
)}
|
|
1192
|
+
</div>
|
|
1193
|
+
)}
|
|
1194
|
+
|
|
1195
|
+
{/* Proposal Preview (if available) */}
|
|
1196
|
+
{proposalMode && proposalPreview && (
|
|
1197
|
+
<div className={styles.proposalPreviewContainer}>
|
|
1198
|
+
<div className={styles.proposalPreviewHeader}>
|
|
1199
|
+
<h3>Proposal Preview</h3>
|
|
1200
|
+
<button
|
|
1201
|
+
className={styles.closePreview}
|
|
1202
|
+
onClick={() => setProposalPreview(null)}
|
|
1203
|
+
aria-label="Close preview"
|
|
1204
|
+
>
|
|
1205
|
+
<X size={16} aria-hidden="true" />
|
|
1206
|
+
</button>
|
|
1207
|
+
</div>
|
|
1208
|
+
<div
|
|
1209
|
+
className={styles.proposalPreview}
|
|
1210
|
+
dangerouslySetInnerHTML={{ __html: proposalPreview }}
|
|
1211
|
+
/>
|
|
1212
|
+
</div>
|
|
1213
|
+
)}
|
|
1214
|
+
|
|
863
1215
|
{/* Quote content */}
|
|
864
1216
|
<div className={styles.quoteContent}>
|
|
865
1217
|
{/* Quote header with logo and date */}
|
|
@@ -245,36 +245,116 @@
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
/* Compact Inline Color Picker Styles */
|
|
249
|
+
.cpicker-inline {
|
|
250
|
+
width: 100%;
|
|
251
|
+
display: flex;
|
|
252
|
+
flex-direction: column;
|
|
253
|
+
gap: 8px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.cpicker-preview-container {
|
|
250
257
|
display: flex;
|
|
251
258
|
align-items: center;
|
|
259
|
+
gap: 8px;
|
|
260
|
+
padding: 6px 10px;
|
|
261
|
+
background: #f8f9fa;
|
|
262
|
+
border: 1px solid #dee2e6;
|
|
263
|
+
border-radius: 6px;
|
|
264
|
+
min-height: 40px;
|
|
265
|
+
}
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
.cpicker__box {
|
|
268
|
+
width: 28px;
|
|
269
|
+
height: 28px;
|
|
270
|
+
border: 2px solid #dee2e6;
|
|
271
|
+
border-radius: 6px;
|
|
272
|
+
background-color: #ffffff;
|
|
273
|
+
position: relative;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
transition: all 0.2s ease;
|
|
276
|
+
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
277
|
+
flex-shrink: 0;
|
|
278
|
+
|
|
279
|
+
/* Checkerboard pattern for transparency */
|
|
280
|
+
background-image:
|
|
281
|
+
linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
|
|
282
|
+
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
|
|
283
|
+
linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
|
|
284
|
+
linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
|
|
285
|
+
background-size: 6px 6px;
|
|
286
|
+
background-position: 0 0, 0 3px, 3px -3px, -3px 0px;
|
|
287
|
+
|
|
288
|
+
/* When no color is selected */
|
|
289
|
+
&[style=""] {
|
|
290
|
+
background:
|
|
291
|
+
linear-gradient(45deg,
|
|
292
|
+
transparent 40%,
|
|
293
|
+
#dc3545 42%,
|
|
294
|
+
#dc3545 58%,
|
|
295
|
+
transparent 60%
|
|
296
|
+
),
|
|
297
|
+
linear-gradient(-45deg,
|
|
298
|
+
transparent 40%,
|
|
299
|
+
#dc3545 42%,
|
|
300
|
+
#dc3545 58%,
|
|
301
|
+
transparent 60%
|
|
302
|
+
),
|
|
303
|
+
#ffffff;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
269
306
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
307
|
+
.cpicker-value {
|
|
308
|
+
font-size: 0.8rem;
|
|
309
|
+
font-weight: 500;
|
|
310
|
+
color: #495057;
|
|
311
|
+
font-family: 'Courier New', monospace;
|
|
312
|
+
background: rgba(255, 255, 255, 0.9);
|
|
313
|
+
padding: 3px 6px;
|
|
314
|
+
border-radius: 3px;
|
|
315
|
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
316
|
+
flex-grow: 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.cpicker-container {
|
|
320
|
+
width: 100%;
|
|
321
|
+
display: flex;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
padding: 8px;
|
|
324
|
+
background: #ffffff;
|
|
325
|
+
border: 1px solid #dee2e6;
|
|
326
|
+
border-radius: 6px;
|
|
327
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
328
|
+
|
|
329
|
+
/* Ensure Sketch picker fits well and is more compact */
|
|
330
|
+
.w-color-sketch {
|
|
331
|
+
box-shadow: none !important;
|
|
332
|
+
border: none !important;
|
|
333
|
+
border-radius: 6px !important;
|
|
334
|
+
max-width: 100% !important;
|
|
335
|
+
transform: scale(0.85) !important;
|
|
336
|
+
transform-origin: center top !important;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Further responsive scaling */
|
|
340
|
+
@media (max-width: 768px) {
|
|
341
|
+
padding: 6px;
|
|
342
|
+
|
|
343
|
+
.w-color-sketch {
|
|
344
|
+
transform: scale(0.75) !important;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@media (max-width: 480px) {
|
|
349
|
+
padding: 4px;
|
|
350
|
+
|
|
351
|
+
.w-color-sketch {
|
|
352
|
+
transform: scale(0.65) !important;
|
|
274
353
|
}
|
|
275
354
|
}
|
|
276
355
|
}
|
|
277
356
|
|
|
357
|
+
|
|
278
358
|
.colour_popover {
|
|
279
359
|
position: absolute;
|
|
280
360
|
background: white;
|