@visns-studio/visns-components 5.11.5 → 5.11.6

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.
@@ -0,0 +1,260 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { X, Search, Tag, User, Building, Calendar, DollarSign, FileText } from 'lucide-react';
3
+ import { defaultProposalApiEndpoints } from '../../utils/urlBuilder';
4
+ import './VariableInserter.css';
5
+
6
+ const defaultVariables = [
7
+ {
8
+ category: 'Customer Information',
9
+ icon: User,
10
+ variables: [
11
+ { name: 'customer_name', description: 'Customer company name' },
12
+ { name: 'customer_contact_name', description: 'Primary contact name' },
13
+ { name: 'customer_contact_email', description: 'Primary contact email' },
14
+ { name: 'customer_contact_phone', description: 'Primary contact phone' },
15
+ { name: 'customer_address', description: 'Customer address' },
16
+ { name: 'customer_abn', description: 'Customer ABN' }
17
+ ]
18
+ },
19
+ {
20
+ category: 'Company Information',
21
+ icon: Building,
22
+ variables: [
23
+ { name: 'company_name', description: 'Your company name' },
24
+ { name: 'company_address', description: 'Your company address' },
25
+ { name: 'company_phone', description: 'Your company phone' },
26
+ { name: 'company_email', description: 'Your company email' },
27
+ { name: 'company_abn', description: 'Your company ABN' },
28
+ { name: 'company_website', description: 'Your company website' }
29
+ ]
30
+ },
31
+ {
32
+ category: 'Quote Details',
33
+ icon: FileText,
34
+ variables: [
35
+ { name: 'quote_number', description: 'Quote reference number' },
36
+ { name: 'quote_date', description: 'Quote creation date' },
37
+ { name: 'quote_valid_until', description: 'Quote expiry date' },
38
+ { name: 'quote_total', description: 'Total quote amount' },
39
+ { name: 'quote_subtotal', description: 'Quote subtotal' },
40
+ { name: 'quote_tax', description: 'Quote tax amount' },
41
+ { name: 'quote_items_table', description: 'Complete quote items table' }
42
+ ]
43
+ },
44
+ {
45
+ category: 'Project Information',
46
+ icon: Calendar,
47
+ variables: [
48
+ { name: 'project_name', description: 'Project title' },
49
+ { name: 'project_description', description: 'Project description' },
50
+ { name: 'project_start_date', description: 'Proposed start date' },
51
+ { name: 'project_end_date', description: 'Proposed end date' },
52
+ { name: 'project_duration', description: 'Project duration' }
53
+ ]
54
+ },
55
+ {
56
+ category: 'Sales Information',
57
+ icon: DollarSign,
58
+ variables: [
59
+ { name: 'salesperson_name', description: 'Sales representative name' },
60
+ { name: 'salesperson_email', description: 'Sales representative email' },
61
+ { name: 'salesperson_phone', description: 'Sales representative phone' },
62
+ { name: 'proposal_date', description: 'Proposal generation date' }
63
+ ]
64
+ }
65
+ ];
66
+
67
+ const VariableInserter = ({
68
+ availableVariables = [],
69
+ onInsert,
70
+ onCancel,
71
+ apiEndpoints = defaultProposalApiEndpoints
72
+ }) => {
73
+ const [searchTerm, setSearchTerm] = useState('');
74
+ const [selectedCategory, setSelectedCategory] = useState('all');
75
+ const [intelligentVariables, setIntelligentVariables] = useState([]);
76
+ const [isLoadingIntelligent, setIsLoadingIntelligent] = useState(false);
77
+ const [useIntelligentVariables, setUseIntelligentVariables] = useState(true);
78
+
79
+ // Load intelligent variables from API
80
+ useEffect(() => {
81
+ const loadIntelligentVariables = async () => {
82
+ if (!useIntelligentVariables) return;
83
+
84
+ try {
85
+ setIsLoadingIntelligent(true);
86
+ const response = await fetch(apiEndpoints.intelligentVariables);
87
+ const data = await response.json();
88
+
89
+ if (data.success && data.variables) {
90
+ setIntelligentVariables(data.variables);
91
+ } else {
92
+ console.warn('No intelligent variables available, falling back to defaults');
93
+ setUseIntelligentVariables(false);
94
+ }
95
+ } catch (error) {
96
+ console.error('Failed to load intelligent variables:', error);
97
+ setUseIntelligentVariables(false);
98
+ } finally {
99
+ setIsLoadingIntelligent(false);
100
+ }
101
+ };
102
+
103
+ loadIntelligentVariables();
104
+ }, [useIntelligentVariables]);
105
+
106
+ // Merge available variables with intelligent variables or defaults
107
+ const allVariables = useMemo(() => {
108
+ if (availableVariables.length > 0) {
109
+ return availableVariables;
110
+ }
111
+
112
+ if (useIntelligentVariables && intelligentVariables.length > 0) {
113
+ return intelligentVariables;
114
+ }
115
+
116
+ return defaultVariables;
117
+ }, [availableVariables, intelligentVariables, useIntelligentVariables]);
118
+
119
+ const filteredVariables = allVariables.map(category => ({
120
+ ...category,
121
+ variables: category.variables.filter(variable =>
122
+ variable.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
123
+ variable.description.toLowerCase().includes(searchTerm.toLowerCase())
124
+ )
125
+ })).filter(category =>
126
+ selectedCategory === 'all' ||
127
+ category.category.toLowerCase().includes(selectedCategory.toLowerCase())
128
+ ).filter(category => category.variables.length > 0);
129
+
130
+ const categories = ['all', ...allVariables.map(cat => cat.category)];
131
+
132
+ return (
133
+ <div className="variable-inserter-overlay">
134
+ <div className="variable-inserter-modal">
135
+ <div className="variable-inserter-header">
136
+ <div>
137
+ <h2 className="variable-inserter-title">Insert Variable</h2>
138
+ <p className="variable-inserter-subtitle">
139
+ Click on a variable to insert it at the cursor position
140
+ {useIntelligentVariables && intelligentVariables.length > 0 && (
141
+ <span className="variable-inserter-intelligent-indicator">
142
+ • Using intelligent model-based variables
143
+ </span>
144
+ )}
145
+ </p>
146
+ {isLoadingIntelligent && (
147
+ <p className="variable-inserter-loading">
148
+ Loading intelligent variables...
149
+ </p>
150
+ )}
151
+ </div>
152
+ <div className="variable-inserter-header-actions">
153
+ {intelligentVariables.length > 0 && (
154
+ <button
155
+ onClick={() => setUseIntelligentVariables(!useIntelligentVariables)}
156
+ className="variable-inserter-toggle-btn"
157
+ title={useIntelligentVariables ? "Switch to default variables" : "Switch to intelligent variables"}
158
+ >
159
+ {useIntelligentVariables ? "Default" : "Smart"}
160
+ </button>
161
+ )}
162
+ <button
163
+ onClick={onCancel}
164
+ className="variable-inserter-close-btn"
165
+ >
166
+ <X size={20} />
167
+ </button>
168
+ </div>
169
+ </div>
170
+
171
+ <div className="variable-inserter-search-section">
172
+ <div className="variable-inserter-search-container">
173
+ {/* Search */}
174
+ <div className="variable-inserter-search-input-wrapper">
175
+ <Search size={20} className="variable-inserter-search-icon" />
176
+ <input
177
+ type="text"
178
+ placeholder="Search variables..."
179
+ value={searchTerm}
180
+ onChange={(e) => setSearchTerm(e.target.value)}
181
+ className="variable-inserter-search-input"
182
+ />
183
+ </div>
184
+
185
+ {/* Category Filter */}
186
+ <select
187
+ value={selectedCategory}
188
+ onChange={(e) => setSelectedCategory(e.target.value)}
189
+ className="variable-inserter-category-select"
190
+ >
191
+ {categories.map(category => (
192
+ <option key={category} value={category}>
193
+ {category === 'all' ? 'All Categories' : category}
194
+ </option>
195
+ ))}
196
+ </select>
197
+ </div>
198
+ </div>
199
+
200
+ <div className="variable-inserter-content">
201
+ {filteredVariables.length === 0 ? (
202
+ <div className="variable-inserter-empty">
203
+ <Tag size={48} className="variable-inserter-empty-icon" />
204
+ <h3 className="variable-inserter-empty-title">No variables found</h3>
205
+ <p className="variable-inserter-empty-text">Try adjusting your search or category filter</p>
206
+ </div>
207
+ ) : (
208
+ <div className="variable-inserter-categories">
209
+ {filteredVariables.map((category) => {
210
+ const IconComponent = category.icon;
211
+ return (
212
+ <div key={category.category} className="variable-inserter-category">
213
+ <div className="variable-inserter-category-header">
214
+ <IconComponent size={20} className="variable-inserter-category-icon" />
215
+ <h3 className="variable-inserter-category-title">
216
+ {category.category}
217
+ </h3>
218
+ </div>
219
+ <div className="variable-inserter-variables-grid">
220
+ {category.variables.map((variable) => (
221
+ <button
222
+ key={variable.name}
223
+ onClick={() => onInsert(variable.name)}
224
+ className="variable-inserter-variable-button"
225
+ >
226
+ <div className="variable-inserter-variable-content">
227
+ <div className="variable-inserter-variable-info">
228
+ <div className="variable-inserter-variable-name">
229
+ {`{{${variable.name}}}`}
230
+ </div>
231
+ <div className="variable-inserter-variable-description">
232
+ {variable.description}
233
+ </div>
234
+ </div>
235
+ <Tag size={16} className="variable-inserter-variable-icon" />
236
+ </div>
237
+ </button>
238
+ ))}
239
+ </div>
240
+ </div>
241
+ );
242
+ })}
243
+ </div>
244
+ )}
245
+ </div>
246
+
247
+ <div className="variable-inserter-footer">
248
+ <button
249
+ onClick={onCancel}
250
+ className="variable-inserter-cancel-btn"
251
+ >
252
+ Cancel
253
+ </button>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ };
259
+
260
+ export default VariableInserter;
@@ -0,0 +1,5 @@
1
+ export { default as ProposalTemplateSectionManager } from './ProposalTemplateSectionManager';
2
+ export { default as SectionEditor } from './SectionEditor';
3
+ export { default as SectionTypeSelector } from './SectionTypeSelector';
4
+ export { default as VariableInserter } from './VariableInserter';
5
+ export { default as ProposalTemplatePreview } from './ProposalTemplatePreview';
@@ -194,7 +194,9 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
194
194
  .inovua-react-toolkit-combo-box__input
195
195
  ):not(.inovua-react-toolkit-date-input__input):not(
196
196
  .inovua-react-toolkit-text-input__input
197
- ):not(.inovua-react-toolkit-numeric-input__input) {
197
+ ):not(.inovua-react-toolkit-numeric-input__input):not(
198
+ .variable-inserter-search-input
199
+ ) {
198
200
  background: white;
199
201
  border-radius: var(--br);
200
202
  border: 1px solid rgba(var(--paragraph-color-rgb), 0.15);
@@ -438,6 +440,182 @@ form div:has(.react-toggle) input[type='checkbox'] {
438
440
  height: 0 !important;
439
441
  }
440
442
 
443
+ /* Relation Color Preview Styles */
444
+ .relation-color-container {
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 8px;
448
+ }
449
+
450
+ .relation-color-container.inline {
451
+ display: inline-flex;
452
+ margin-right: 4px;
453
+ }
454
+
455
+ .relation-color-preview {
456
+ width: 20px;
457
+ height: 20px;
458
+ border: 2px solid #dee2e6;
459
+ border-radius: 4px;
460
+ background-color: #ffffff;
461
+ position: relative;
462
+ overflow: hidden;
463
+ flex-shrink: 0;
464
+ cursor: pointer;
465
+ transition: all 0.2s ease;
466
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
467
+
468
+ /* Checkerboard pattern for transparency */
469
+ background-image:
470
+ linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
471
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
472
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
473
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
474
+ background-size: 4px 4px;
475
+ background-position: 0 0, 0 2px, 2px -2px, -2px 0px;
476
+ }
477
+
478
+ .relation-color-preview:hover {
479
+ border-color: #adb5bd;
480
+ transform: scale(1.1);
481
+ box-shadow:
482
+ inset 0 1px 2px rgba(0, 0, 0, 0.1),
483
+ 0 2px 8px rgba(0, 0, 0, 0.15);
484
+ }
485
+
486
+ .relation-color-text {
487
+ font-size: 0.875rem;
488
+ font-weight: 500;
489
+ color: #495057;
490
+ font-family: 'Courier New', monospace;
491
+ user-select: all;
492
+ }
493
+
494
+ .relation-array-container {
495
+ display: flex;
496
+ flex-wrap: wrap;
497
+ align-items: center;
498
+ gap: 4px;
499
+ }
500
+
501
+ .separator {
502
+ color: #6c757d;
503
+ margin: 0 2px;
504
+ }
505
+
506
+ /* Compact Inline Color Picker Styles for Forms */
507
+ .cpicker-inline {
508
+ width: 100%;
509
+ display: flex;
510
+ flex-direction: column;
511
+ gap: 8px;
512
+ }
513
+
514
+ .cpicker-preview-container {
515
+ display: flex;
516
+ align-items: center;
517
+ gap: 8px;
518
+ padding: 6px 10px;
519
+ background: #f8f9fa;
520
+ border: 1px solid #dee2e6;
521
+ border-radius: 6px;
522
+ min-height: 40px;
523
+ }
524
+
525
+ .cpicker__box {
526
+ width: 28px;
527
+ height: 28px;
528
+ border: 2px solid #dee2e6;
529
+ border-radius: 6px;
530
+ background-color: #ffffff;
531
+ position: relative;
532
+ overflow: hidden;
533
+ transition: all 0.2s ease;
534
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
535
+ flex-shrink: 0;
536
+
537
+ /* Checkerboard pattern for transparency */
538
+ background-image:
539
+ linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
540
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
541
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
542
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
543
+ background-size: 6px 6px;
544
+ background-position: 0 0, 0 3px, 3px -3px, -3px 0px;
545
+ }
546
+
547
+ /* When no color is selected */
548
+ .cpicker__box[style=""] {
549
+ background:
550
+ linear-gradient(45deg,
551
+ transparent 40%,
552
+ #dc3545 42%,
553
+ #dc3545 58%,
554
+ transparent 60%
555
+ ),
556
+ linear-gradient(-45deg,
557
+ transparent 40%,
558
+ #dc3545 42%,
559
+ #dc3545 58%,
560
+ transparent 60%
561
+ ),
562
+ #ffffff;
563
+ }
564
+
565
+ .cpicker-value {
566
+ font-size: 0.8rem;
567
+ font-weight: 500;
568
+ color: #495057;
569
+ font-family: 'Courier New', monospace;
570
+ background: rgba(255, 255, 255, 0.9);
571
+ padding: 3px 6px;
572
+ border-radius: 3px;
573
+ border: 1px solid rgba(0, 0, 0, 0.1);
574
+ flex-grow: 1;
575
+ }
576
+
577
+ .cpicker-container {
578
+ width: 100%;
579
+ display: flex;
580
+ justify-content: center;
581
+ padding: 8px;
582
+ background: #ffffff;
583
+ border: 1px solid #dee2e6;
584
+ border-radius: 6px;
585
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
586
+ }
587
+
588
+ /* Ensure Sketch picker fits well and is more compact */
589
+ .cpicker-container .w-color-sketch {
590
+ box-shadow: none !important;
591
+ border: none !important;
592
+ border-radius: 6px !important;
593
+ max-width: 100% !important;
594
+ transform: scale(0.85) !important;
595
+ transform-origin: center top !important;
596
+ }
597
+
598
+ /* Further responsive scaling for color picker */
599
+ @media (max-width: 768px) {
600
+ .cpicker-container {
601
+ padding: 6px;
602
+ }
603
+
604
+ .cpicker-container .w-color-sketch {
605
+ transform: scale(0.75) !important;
606
+ }
607
+ }
608
+
609
+ @media (max-width: 480px) {
610
+ .cpicker-container {
611
+ padding: 4px;
612
+ }
613
+
614
+ .cpicker-container .w-color-sketch {
615
+ transform: scale(0.65) !important;
616
+ }
617
+ }
618
+
441
619
  /* Safari-specific fixes for form elements */
442
620
  @supports (-webkit-touch-callout: none) {
443
621
  /* Safari-specific styles */
package/src/index.js CHANGED
@@ -65,6 +65,14 @@ import GenericReport from './components/crm/generic/GenericReport';
65
65
  import GenericSort from './components/crm/generic/GenericSort';
66
66
  import NotificationList from './components/crm/generic/NotificationList';
67
67
 
68
+ /** Proposal Components */
69
+ import ProposalTemplateSectionManager from './components/proposal/ProposalTemplateSectionManager';
70
+ import SectionEditor from './components/proposal/SectionEditor';
71
+ import SectionTypeSelector from './components/proposal/SectionTypeSelector';
72
+ import VariableInserter from './components/proposal/VariableInserter';
73
+ import ProposalTemplatePreview from './components/proposal/ProposalTemplatePreview';
74
+
75
+
68
76
  export {
69
77
  AsyncSelect,
70
78
  AssociationManager,
@@ -115,11 +123,16 @@ export {
115
123
  Notification,
116
124
  NotificationList,
117
125
  Profile,
126
+ ProposalTemplateSectionManager,
127
+ ProposalTemplatePreview,
118
128
  Reset,
129
+ SectionEditor,
130
+ SectionTypeSelector,
119
131
  SelectList,
120
132
  Select,
121
133
  SortableList,
122
134
  Table,
123
135
  TableFilter,
136
+ VariableInserter,
124
137
  Verify,
125
138
  };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Build URL by replacing template variables with actual values
3
+ * @param {string} urlTemplate - URL template with placeholders like /ajax/{templateId}/sections/{sectionId}
4
+ * @param {object} params - Object containing parameter values
5
+ * @returns {string} URL with placeholders replaced
6
+ */
7
+ export const buildUrl = (urlTemplate, params = {}) => {
8
+ let url = urlTemplate;
9
+
10
+ Object.entries(params).forEach(([key, value]) => {
11
+ url = url.replace(`{${key}}`, value);
12
+ });
13
+
14
+ return url;
15
+ };
16
+
17
+ /**
18
+ * Default API endpoints configuration for proposal system
19
+ */
20
+ export const defaultProposalApiEndpoints = {
21
+ // Variable management
22
+ intelligentVariables: '/ajax/proposalTemplates/variables/intelligent',
23
+ availableVariables: '/ajax/proposalTemplates/variables/available',
24
+
25
+ // Template management
26
+ templateData: '/ajax/proposalTemplates/{templateId}',
27
+ templatePreview: '/ajax/proposalTemplates/{templateId}/preview',
28
+ templatePdf: '/ajax/proposalTemplates/{templateId}/pdf',
29
+ templateFullPreview: '/ajax/proposalTemplates/{templateId}/preview',
30
+
31
+ // Section management
32
+ sectionsCreate: '/ajax/proposalTemplates/{templateId}/sections',
33
+ sectionsUpdate: '/ajax/proposalTemplates/{templateId}/sections/{sectionId}',
34
+ sectionsDelete: '/ajax/proposalTemplates/{templateId}/sections/{sectionId}',
35
+ sectionsReorder: '/ajax/proposalTemplates/{templateId}/sections/reorder',
36
+ sectionsToggle: '/ajax/proposalTemplates/{templateId}/sections/{sectionId}',
37
+
38
+ // Branding profiles
39
+ brandingProfiles: '/ajax/brandingProfiles/dropdown'
40
+ };