@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.
package/README.md CHANGED
@@ -2350,6 +2350,359 @@ We welcome contributions! Please follow these steps:
2350
2350
 
2351
2351
  [MIT License](LICENSE)
2352
2352
 
2353
+ ### Quote Management Components
2354
+
2355
+ #### GenericQuote (Enhanced with Proposal System v5.12.0+)
2356
+
2357
+ The GenericQuote component provides comprehensive quote management with integrated proposal generation capabilities. It supports both traditional quote display and full proposal creation with cover pages, table of contents, and customizable sections.
2358
+
2359
+ ```jsx
2360
+ <GenericQuote
2361
+ config={{
2362
+ // Basic quote configuration
2363
+ data: quoteData,
2364
+ currency: "AUD",
2365
+ companyLogo: "/path/to/logo.png",
2366
+
2367
+ // Standard features
2368
+ showLineItems: true,
2369
+ allowEditing: true,
2370
+ showNotes: true,
2371
+
2372
+ // Enhanced proposal features (v5.12.0+)
2373
+ proposalMode: {
2374
+ enabled: true,
2375
+ templateId: 1,
2376
+ brandingProfileId: 2,
2377
+ customSections: [
2378
+ {
2379
+ type: 'overview',
2380
+ title: 'Project Overview',
2381
+ content: '<h1>Executive Summary</h1><p>This proposal outlines...</p>'
2382
+ },
2383
+ {
2384
+ type: 'terms_conditions',
2385
+ title: 'Terms & Conditions',
2386
+ content: '<h3>Payment Terms</h3><p>Net 30 days...</p>'
2387
+ }
2388
+ ]
2389
+ }
2390
+ }}
2391
+ userProfile={userProfile}
2392
+ onQuoteSave={(quoteData) => handleQuoteSave(quoteData)}
2393
+ onProposalGenerate={(proposalData) => handleProposalGeneration(proposalData)}
2394
+ />
2395
+ ```
2396
+
2397
+ **New Proposal Features:**
2398
+
2399
+ 1. **Proposal Mode Toggle**:
2400
+ - Seamlessly switch between quote view and proposal generation mode
2401
+ - Maintains full backward compatibility with existing quote functionality
2402
+ - Context-aware interface that adapts based on proposal configuration
2403
+
2404
+ 2. **Template Management Integration**:
2405
+ - Select from pre-configured proposal templates
2406
+ - Template preview with variable substitution
2407
+ - Custom section creation and ordering
2408
+ - Dynamic content population from quote data
2409
+
2410
+ 3. **Branding Profile Support**:
2411
+ - Company logo and branding integration
2412
+ - Custom color schemes and styling
2413
+ - Professional cover page generation
2414
+ - Consistent brand presentation
2415
+
2416
+ 4. **Advanced Section Management**:
2417
+ - **Cover Page**: Company branding with proposal title and customer details
2418
+ - **Table of Contents**: Auto-generated from all proposal sections
2419
+ - **Overview**: Executive summary with custom headers (H1, H2, H3) and content
2420
+ - **Quote Items**: Pricing table following original quote structure
2421
+ - **Terms & Conditions**: Static legal terms (rarely changes)
2422
+ - **Payment Terms**: Dynamic payment schedules and due dates
2423
+ - **Review Log**: Version history and change tracking
2424
+ - **Agreement & Signatures**: Static signature section for approvals
2425
+
2426
+ 5. **Dynamic Content System**:
2427
+ - Variable replacement throughout all sections
2428
+ - Real-time preview of populated content
2429
+ - Support for both static and dynamic sections
2430
+ - Custom variable definitions per template
2431
+
2432
+ **Configuration Options:**
2433
+
2434
+ ```jsx
2435
+ const proposalConfig = {
2436
+ // Enable proposal features
2437
+ enabled: true,
2438
+
2439
+ // Template selection
2440
+ templateId: 1, // Selected proposal template
2441
+ allowTemplateChange: true, // Allow template switching
2442
+
2443
+ // Branding configuration
2444
+ brandingProfileId: 2, // Selected branding profile
2445
+ allowBrandingChange: true, // Allow branding changes
2446
+
2447
+ // Section customization
2448
+ customSections: [
2449
+ {
2450
+ type: 'overview',
2451
+ title: 'Project Overview',
2452
+ content: '<h1>Executive Summary</h1><p>Custom content...</p>',
2453
+ variables: {
2454
+ 'project_scope': 'Website development and maintenance',
2455
+ 'timeline': '8-12 weeks'
2456
+ }
2457
+ }
2458
+ ],
2459
+
2460
+ // Generation settings
2461
+ autoGenerateTOC: true, // Auto-generate table of contents
2462
+ includePageNumbers: true, // Add page numbers to PDF
2463
+ watermark: false, // Add watermark to pages
2464
+
2465
+ // PDF export options
2466
+ paperSize: 'A4', // Paper size for PDF generation
2467
+ orientation: 'portrait', // Page orientation
2468
+ margins: '40px', // Page margins
2469
+
2470
+ // UI customization
2471
+ showPreview: true, // Show live preview panel
2472
+ previewMode: 'split', // 'split', 'modal', 'tabs'
2473
+ enableSectionReorder: true, // Allow section drag-and-drop
2474
+ showVariableHelper: true, // Show available variables
2475
+ };
2476
+ ```
2477
+
2478
+ **Available Variables for Dynamic Content:**
2479
+
2480
+ The proposal system supports these built-in variables that are automatically populated from quote and customer data:
2481
+
2482
+ ```javascript
2483
+ // Customer Information
2484
+ {{customer_name}} // Customer company name
2485
+ {{customer_address}} // Customer full address
2486
+ {{customer_contact}} // Primary contact name
2487
+ {{customer_email}} // Primary contact email
2488
+ {{customer_phone}} // Primary contact phone
2489
+
2490
+ // Quote Information
2491
+ {{quote_number}} // Quote/proposal number
2492
+ {{quote_date}} // Quote creation date
2493
+ {{quote_valid_until}} // Quote expiration date
2494
+ {{total_amount}} // Total quote amount (formatted)
2495
+ {{total_ex_gst}} // Total excluding GST
2496
+ {{total_gst}} // GST amount
2497
+ {{currency}} // Currency symbol/code
2498
+
2499
+ // Company Information
2500
+ {{company_name}} // Your company name
2501
+ {{company_address}} // Your company address
2502
+ {{company_phone}} // Your company phone
2503
+ {{company_email}} // Your company email
2504
+ {{company_website}} // Your company website
2505
+ {{company_abn}} // Australian Business Number
2506
+
2507
+ // Project Information
2508
+ {{project_manager}} // Assigned project manager
2509
+ {{project_timeline}} // Estimated timeline
2510
+ {{project_scope}} // Project scope description
2511
+
2512
+ // Date Variables
2513
+ {{current_date}} // Current date
2514
+ {{due_date}} // Payment due date
2515
+ {{start_date}} // Project start date
2516
+
2517
+ // Item-Specific Variables
2518
+ {{items_onceoff}} // One-time purchase items table
2519
+ {{items_monthly}} // Monthly subscription items table
2520
+ {{items_yearly}} // Yearly subscription items table
2521
+ {{line_items_total}} // Total number of line items
2522
+ ```
2523
+
2524
+ **Integration with visns-packages Backend:**
2525
+
2526
+ The GenericQuote component seamlessly integrates with the visns-packages proposal system:
2527
+
2528
+ ```jsx
2529
+ // Backend integration endpoints
2530
+ const proposalEndpoints = {
2531
+ // Template management
2532
+ templates: '/ajax/proposal-templates',
2533
+ templatePreview: '/ajax/proposal-templates/{id}/preview',
2534
+
2535
+ // Branding profiles
2536
+ brandingProfiles: '/ajax/branding-profiles',
2537
+ brandingCSS: '/ajax/branding-profiles/{id}/css',
2538
+
2539
+ // PDF generation
2540
+ generatePDF: '/ajax/pdf/generate-proposal',
2541
+ previewHTML: '/ajax/pdf/preview-proposal',
2542
+
2543
+ // Variable helpers
2544
+ availableVariables: '/ajax/proposal-templates/variables/available'
2545
+ };
2546
+ ```
2547
+
2548
+ **Event Handling:**
2549
+
2550
+ ```jsx
2551
+ <GenericQuote
2552
+ config={quoteConfig}
2553
+ // Quote events (existing)
2554
+ onQuoteSave={(data) => console.log('Quote saved:', data)}
2555
+ onLineItemChange={(items) => console.log('Line items updated:', items)}
2556
+
2557
+ // New proposal events
2558
+ onProposalGenerate={(proposalData) => {
2559
+ console.log('Proposal generated:', proposalData);
2560
+ // Handle PDF generation or preview
2561
+ }}
2562
+ onTemplateChange={(templateId) => {
2563
+ console.log('Template changed to:', templateId);
2564
+ // Update template-specific settings
2565
+ }}
2566
+ onBrandingChange={(brandingId) => {
2567
+ console.log('Branding changed to:', brandingId);
2568
+ // Update branding-specific styling
2569
+ }}
2570
+ onSectionUpdate={(sections) => {
2571
+ console.log('Sections updated:', sections);
2572
+ // Handle custom section changes
2573
+ }}
2574
+ onVariableChange={(variables) => {
2575
+ console.log('Variables updated:', variables);
2576
+ // Handle variable substitution updates
2577
+ }}
2578
+ />
2579
+ ```
2580
+
2581
+ **Backward Compatibility:**
2582
+
2583
+ All existing GenericQuote functionality remains unchanged:
2584
+ - Standard quote display and editing
2585
+ - Line item management
2586
+ - Notes and attachments
2587
+ - Export capabilities
2588
+ - Custom actions and workflows
2589
+
2590
+ Proposal features are **opt-in** and activated only when `proposalConfig.enabled: true` is set.
2591
+
2592
+ **Advanced Usage Example:**
2593
+
2594
+ ```jsx
2595
+ import React, { useState } from 'react';
2596
+ import { GenericQuote } from '@visns-studio/visns-components';
2597
+
2598
+ const QuoteProposalManager = () => {
2599
+ const [quoteData, setQuoteData] = useState({
2600
+ id: 1,
2601
+ number: 'Q-2024-001',
2602
+ customer: {
2603
+ name: 'Acme Corporation',
2604
+ address: '123 Business St, Sydney NSW 2000',
2605
+ contact: 'John Smith',
2606
+ email: 'john@acme.com'
2607
+ },
2608
+ items: [
2609
+ {
2610
+ description: 'Website Development',
2611
+ quantity: 1,
2612
+ rate: 5000,
2613
+ total: 5000,
2614
+ type: 'onceoff'
2615
+ },
2616
+ {
2617
+ description: 'Monthly Hosting',
2618
+ quantity: 1,
2619
+ rate: 99,
2620
+ total: 99,
2621
+ type: 'monthly'
2622
+ }
2623
+ ],
2624
+ total: 5099
2625
+ });
2626
+
2627
+ const [proposalMode, setProposalMode] = useState(false);
2628
+
2629
+ const proposalConfig = {
2630
+ enabled: true,
2631
+ templateId: 1,
2632
+ brandingProfileId: 1,
2633
+ customSections: [
2634
+ {
2635
+ type: 'overview',
2636
+ title: 'Project Overview',
2637
+ content: `
2638
+ <h1>Executive Summary</h1>
2639
+ <p>This proposal outlines our recommended solution for {{customer_name}}.</p>
2640
+
2641
+ <h2>Project Scope</h2>
2642
+ <p>We will develop a modern, responsive website that meets your business requirements.</p>
2643
+
2644
+ <h3>Key Deliverables</h3>
2645
+ <ul>
2646
+ <li>Custom website design and development</li>
2647
+ <li>Content management system</li>
2648
+ <li>Mobile-responsive design</li>
2649
+ <li>SEO optimization</li>
2650
+ </ul>
2651
+ `
2652
+ }
2653
+ ],
2654
+ autoGenerateTOC: true,
2655
+ showPreview: true,
2656
+ previewMode: 'split'
2657
+ };
2658
+
2659
+ return (
2660
+ <div className="quote-proposal-manager">
2661
+ <div className="mode-toggle">
2662
+ <button
2663
+ onClick={() => setProposalMode(false)}
2664
+ className={!proposalMode ? 'active' : ''}
2665
+ >
2666
+ Quote View
2667
+ </button>
2668
+ <button
2669
+ onClick={() => setProposalMode(true)}
2670
+ className={proposalMode ? 'active' : ''}
2671
+ >
2672
+ Proposal Builder
2673
+ </button>
2674
+ </div>
2675
+
2676
+ <GenericQuote
2677
+ config={{
2678
+ data: quoteData,
2679
+ currency: "AUD",
2680
+ showLineItems: true,
2681
+ allowEditing: !proposalMode,
2682
+
2683
+ // Enable proposal features based on mode
2684
+ proposalMode: proposalMode ? proposalConfig : { enabled: false }
2685
+ }}
2686
+ userProfile={userProfile}
2687
+ onQuoteSave={(data) => {
2688
+ setQuoteData(data);
2689
+ console.log('Quote updated:', data);
2690
+ }}
2691
+ onProposalGenerate={(proposalData) => {
2692
+ // Generate PDF or handle proposal data
2693
+ console.log('Generating proposal PDF:', proposalData);
2694
+
2695
+ // Example: Download PDF
2696
+ window.open(`/ajax/pdf/generate-proposal?quote_id=${quoteData.id}`, '_blank');
2697
+ }}
2698
+ />
2699
+ </div>
2700
+ );
2701
+ };
2702
+
2703
+ export default QuoteProposalManager;
2704
+ ```
2705
+
2353
2706
  ## Modal Actions in Components
2354
2707
 
2355
2708
  ### Using Modal Actions in GenericQuote Component
package/package.json CHANGED
@@ -13,6 +13,7 @@
13
13
  "@nivo/pie": "^0.99.0",
14
14
  "@tinymce/miniature": "^6.0.0",
15
15
  "@tinymce/tinymce-react": "^6.2.1",
16
+ "@uiw/react-color": "^2.6.0",
16
17
  "@visns-studio/visns-datagrid-community": "^1.0.14",
17
18
  "@visns-studio/visns-datagrid-enterprise": "^1.0.14",
18
19
  "@vitejs/plugin-react": "^4.5.2",
@@ -27,15 +28,14 @@
27
28
  "html-react-parser": "^5.2.5",
28
29
  "lodash": "^4.17.21",
29
30
  "lodash.debounce": "^4.0.8",
30
- "lucide-react": "^0.516.0",
31
+ "lucide-react": "^0.518.0",
31
32
  "moment": "^2.30.1",
32
33
  "motion": "^12.18.1",
33
34
  "numeral": "^2.0.6",
34
35
  "pluralize": "^8.0.0",
35
36
  "qrcode.react": "^4.2.0",
36
37
  "quill-image-uploader": "^1.3.0",
37
- "react-big-calendar": "^1.19.3",
38
- "react-color": "^2.19.3",
38
+ "react-big-calendar": "^1.19.4",
39
39
  "react-copy-to-clipboard": "^5.1.0",
40
40
  "react-datepicker": "^8.4.0",
41
41
  "react-dropzone": "^14.3.8",
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.11.5",
90
+ "version": "5.11.6",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -108,5 +108,5 @@
108
108
  "url": "https://github.com/visnsstudio/visns-components/issues"
109
109
  },
110
110
  "homepage": "https://github.com/visnsstudio/visns-components#readme",
111
- "packageManager": "yarn@4.9.1"
111
+ "packageManager": "yarn@4.9.2"
112
112
  }
@@ -7,7 +7,7 @@ import Toggle from 'react-toggle';
7
7
  import moment from 'moment';
8
8
  import parse from 'html-react-parser';
9
9
  import Popup from 'reactjs-popup';
10
- import { BlockPicker } from 'react-color';
10
+ import { Sketch } from '@uiw/react-color';
11
11
  import { CopyToClipboard } from 'react-copy-to-clipboard';
12
12
  import { NumericFormat, PatternFormat } from 'react-number-format';
13
13
  import { Editor } from '@tinymce/tinymce-react';
@@ -110,12 +110,13 @@ function Field({
110
110
  right: '0px',
111
111
  bottom: '0px',
112
112
  left: '0px',
113
+ zIndex: '999998',
113
114
  };
114
115
  const popover = {
115
116
  position: 'absolute',
116
- zIndex: '2',
117
- left: '20px',
118
- marginTop: '6px',
117
+ zIndex: '999999',
118
+ left: '0',
119
+ marginTop: '8px',
119
120
  };
120
121
 
121
122
  useEffect(() => {
@@ -456,6 +457,12 @@ function Field({
456
457
  }
457
458
  };
458
459
 
460
+ // Utility function to detect if a string is a valid hex color
461
+ const isHexColor = (str) => {
462
+ if (!str || typeof str !== 'string') return false;
463
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(str);
464
+ };
465
+
459
466
  const renderRelationText = (value) => {
460
467
  let text = '';
461
468
 
@@ -473,23 +480,56 @@ function Field({
473
480
  text = processKeys(value);
474
481
  }
475
482
 
483
+ // If the text is a hex color, render it with a color preview
484
+ if (isHexColor(text)) {
485
+ return (
486
+ <div className="relation-color-container">
487
+ <div
488
+ className="relation-color-preview"
489
+ style={{ backgroundColor: text }}
490
+ title={`Color: ${text}`}
491
+ ></div>
492
+ <span className="relation-color-text">{text}</span>
493
+ </div>
494
+ );
495
+ }
496
+
476
497
  return text;
477
498
  };
478
499
 
479
500
  const renderRelationArrayText = (value) => {
480
- let text = '';
481
-
482
501
  if (value && settings.nameFrom) {
483
- value.forEach((a, b) => {
484
- if (b > 0) {
485
- text += ', ';
486
- }
487
-
488
- text += a[settings.nameFrom];
489
- });
502
+ return (
503
+ <div className="relation-array-container">
504
+ {value.map((item, index) => {
505
+ const itemValue = item[settings.nameFrom];
506
+
507
+ if (isHexColor(itemValue)) {
508
+ return (
509
+ <div key={index} className="relation-color-container inline">
510
+ <div
511
+ className="relation-color-preview"
512
+ style={{ backgroundColor: itemValue }}
513
+ title={`Color: ${itemValue}`}
514
+ ></div>
515
+ <span className="relation-color-text">{itemValue}</span>
516
+ {index < value.length - 1 && <span className="separator">, </span>}
517
+ </div>
518
+ );
519
+ }
520
+
521
+ return (
522
+ <span key={index}>
523
+ {itemValue}
524
+ {index < value.length - 1 && ', '}
525
+ </span>
526
+ );
527
+ })}
528
+ </div>
529
+ );
490
530
  }
491
531
 
492
- return text;
532
+ return '';
493
533
  };
494
534
 
495
535
  const renderInput = () => {
@@ -624,52 +664,41 @@ function Field({
624
664
  );
625
665
  case 'colour':
626
666
  return (
627
- <div className="cpicker">
628
- <div
629
- className="cpicker__box"
630
- style={
631
- inputValue && inputValue !== 'null'
632
- ? { background: inputValue }
633
- : {}
634
- }
635
- ></div>
636
- <div className="cpicker__btn">
637
- <button
638
- onClick={(e) => {
639
- e.preventDefault();
640
-
641
- setColorPickerShow(
642
- colorPickerShow === true ? false : true
667
+ <div className="cpicker-inline">
668
+ <div className="cpicker-preview-container">
669
+ <div
670
+ className="cpicker__box"
671
+ data-color={inputValue && inputValue !== 'null' ? inputValue : 'No colour selected'}
672
+ style={
673
+ inputValue && inputValue !== 'null'
674
+ ? { backgroundColor: inputValue }
675
+ : {}
676
+ }
677
+ title={inputValue && inputValue !== 'null' ? `Current colour: ${inputValue}` : 'No colour selected'}
678
+ ></div>
679
+ <span className="cpicker-value">
680
+ {inputValue && inputValue !== 'null' ? inputValue : 'No colour selected'}
681
+ </span>
682
+ </div>
683
+ <div className="cpicker-container">
684
+ <Sketch
685
+ color={
686
+ inputValue && inputValue !== 'null'
687
+ ? inputValue
688
+ : '#000000'
689
+ }
690
+ onChange={(color) => {
691
+ onChangeColour(
692
+ { target: { value: color.hex } },
693
+ { hex: color.hex },
694
+ settings.id
643
695
  );
644
696
  }}
645
- >
646
- Pick Color
647
- </button>
648
- {colorPickerShow === true ? (
649
- <div style={popover}>
650
- <div
651
- style={cover}
652
- onClick={() => {
653
- setColorPickerShow(false);
654
- }}
655
- />
656
-
657
- <BlockPicker
658
- color={
659
- inputValue && inputValue !== 'null'
660
- ? inputValue
661
- : ''
662
- }
663
- onChangeComplete={(color, event) => {
664
- onChangeColour(
665
- event,
666
- color,
667
- settings.id
668
- );
669
- }}
670
- />
671
- </div>
672
- ) : null}
697
+ style={{
698
+ boxShadow: 'none',
699
+ border: 'none'
700
+ }}
701
+ />
673
702
  </div>
674
703
  </div>
675
704
  );
@@ -1981,13 +2010,12 @@ function Field({
1981
2010
  'media',
1982
2011
  'table',
1983
2012
  'code',
1984
- 'hr',
1985
2013
  ],
1986
2014
  toolbar:
1987
2015
  'insertTextDropdown | undo redo | blocks | ' +
1988
2016
  'bold italic forecolor | alignleft aligncenter ' +
1989
2017
  'alignright alignjustify | bullist numlist outdent indent | ' +
1990
- 'hr | removeformat | table | code',
2018
+ 'removeformat | table | code',
1991
2019
  content_style:
1992
2020
  'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
1993
2021
  setup: (editor) => {
@@ -767,6 +767,25 @@ export const renderRelationColumn = ({
767
767
  typeof value === 'string' ||
768
768
  typeof value === 'number'
769
769
  ) {
770
+ // Check if the value is a hex color and render with color preview
771
+ const isHexColor = (str) => {
772
+ if (!str || typeof str !== 'string') return false;
773
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(str);
774
+ };
775
+
776
+ if (isHexColor(value)) {
777
+ return (
778
+ <div className="relation-color-container">
779
+ <div
780
+ className="relation-color-preview"
781
+ style={{ backgroundColor: value }}
782
+ title={`Color: ${value}`}
783
+ ></div>
784
+ <span className="relation-color-text">{value}</span>
785
+ </div>
786
+ );
787
+ }
788
+
770
789
  return (
771
790
  <CellWithTooltip
772
791
  value={value}