@visns-studio/visns-components 5.15.7 → 5.15.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.
@@ -3,6 +3,9 @@ import mapboxgl from 'mapbox-gl';
3
3
  import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
4
4
  import html2canvas from 'html2canvas';
5
5
  import Swal from 'sweetalert2';
6
+ import { pdf } from '@react-pdf/renderer';
7
+ import CameraReportPDF from './utils/CameraReportPDF';
8
+ import SimpleCameraReportPDF from './utils/SimpleCameraReportPDF';
6
9
  import {
7
10
  Edit3,
8
11
  Trash2,
@@ -455,377 +458,160 @@ const CameraPlacement = () => {
455
458
  input.click();
456
459
  }, []);
457
460
 
458
- const exportMap = useCallback(async () => {
461
+ const exportToPDF = useCallback(async () => {
459
462
  try {
460
463
  if (!cameras.length) {
461
464
  Swal.fire({
462
465
  icon: 'warning',
463
466
  title: 'No Cameras',
464
467
  text: 'Please add cameras first before exporting.',
465
- confirmButtonColor: '#10b981',
468
+ confirmButtonColor: '#1e40af',
466
469
  });
467
470
  return;
468
471
  }
469
472
 
470
- const imageContainer = document.querySelector(
471
- `.${styles.imageContainer}`
472
- );
473
- if (!imageContainer) {
474
- Swal.fire({
475
- icon: 'warning',
476
- title: 'No Image',
477
- text: 'Please upload an image first before exporting.',
478
- confirmButtonColor: '#10b981',
479
- });
480
- return;
481
- }
482
-
483
- // First, take a screenshot of just the image container to get its natural size
484
- const imageCanvas = await html2canvas(imageContainer, {
485
- useCORS: true,
486
- allowTaint: true,
487
- scale: 1.2,
488
- backgroundColor: '#f8fafc',
489
- logging: false,
473
+ // Show loading message
474
+ Swal.fire({
475
+ icon: 'info',
476
+ title: 'Generating PDF...',
477
+ text: 'Please wait while we create your professional camera installation report.',
478
+ allowOutsideClick: false,
479
+ allowEscapeKey: false,
480
+ showConfirmButton: false,
481
+ willOpen: () => {
482
+ Swal.showLoading();
483
+ },
490
484
  });
491
485
 
492
- // Calculate dimensions for professional report layout
493
- const cameraListWidth = 550; // Fixed width for professional report
494
- const headerHeight = 140; // Space for report header
495
- const summaryHeight = 120; // Space for project summary
496
- const cameraHeight = 180; // More space per camera card
497
- const footerHeight = 300; // Space for legend and notes
498
- const totalCameraListHeight =
499
- headerHeight +
500
- summaryHeight +
501
- cameras.length * cameraHeight +
502
- footerHeight;
503
-
504
- // Use the larger of image height or camera list height
505
- const finalHeight = Math.max(
506
- imageCanvas.height,
507
- totalCameraListHeight
508
- );
509
- const finalWidth = imageCanvas.width + cameraListWidth + 20; // small gap
510
-
511
- // Create the composite container with calculated dimensions
512
- const exportContainer = document.createElement('div');
513
- exportContainer.style.position = 'absolute';
514
- exportContainer.style.top = '-10000px';
515
- exportContainer.style.left = '-10000px';
516
- exportContainer.style.width = finalWidth + 'px';
517
- exportContainer.style.height = finalHeight + 'px';
518
- exportContainer.style.backgroundColor = '#ffffff';
519
- exportContainer.style.display = 'flex';
520
- exportContainer.style.gap = '10px';
521
- exportContainer.style.fontFamily =
522
- '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif';
523
- document.body.appendChild(exportContainer);
524
-
525
- // Left side: Image
526
- const imageDiv = document.createElement('div');
527
- imageDiv.style.width = imageCanvas.width + 'px';
528
- imageDiv.style.height = finalHeight + 'px';
529
- imageDiv.style.background = '#f8fafc';
530
- imageDiv.style.display = 'flex';
531
- imageDiv.style.alignItems = 'center';
532
- imageDiv.style.justifyContent = 'center';
533
- imageDiv.style.borderRight = '2px solid #e5e7eb';
534
- imageDiv.style.flexShrink = '0';
535
-
536
- // Scale the canvas to fit if needed
537
- const scaledCanvas = document.createElement('canvas');
538
- const ctx = scaledCanvas.getContext('2d');
539
- scaledCanvas.width = imageCanvas.width;
540
- scaledCanvas.height = imageCanvas.height;
541
- ctx.drawImage(imageCanvas, 0, 0);
542
-
543
- imageDiv.appendChild(scaledCanvas);
544
- exportContainer.appendChild(imageDiv);
545
-
546
- // Right side: Camera list
547
- const cameraListDiv = document.createElement('div');
548
- cameraListDiv.style.width = cameraListWidth + 'px';
549
- cameraListDiv.style.height = finalHeight + 'px';
550
- cameraListDiv.style.padding = '20px';
551
- cameraListDiv.style.background = '#ffffff';
552
- cameraListDiv.style.overflow = 'visible';
553
- cameraListDiv.style.boxSizing = 'border-box';
554
-
555
- // Generate professional report-style HTML
556
- const cameraListHTML = `
557
- <!-- Report Header -->
558
- <div style="background: linear-gradient(135deg, #374151 0%, #1f2937 100%); color: white; padding: 32px; margin: -20px -20px 0 -20px; border-radius: 16px 16px 0 0; box-shadow: 0 6px 20px rgba(0,0,0,0.15);">
559
- <h1 style="margin: 0 0 16px 0; font-size: 36px; font-weight: 800; text-align: center; color: #ffffff; text-shadow: 0 2px 8px rgba(0,0,0,0.5); letter-spacing: -0.6px; line-height: 1.1;">Security Camera Installation Report</h1>
560
- <div style="text-align: center; margin-bottom: 24px;">
561
- <div style="display: inline-block; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); padding: 12px 24px; border-radius: 25px; font-size: 14px; font-weight: 600; letter-spacing: 1px; color: #e5e7eb;">
562
- PROJECT LAYOUT & SPECIFICATIONS
563
- </div>
564
- </div>
565
- <div style="display: flex; justify-content: space-between; align-items: center; font-size: 14px; border-top: 1px solid rgba(255,255,255,0.15); padding-top: 20px; letter-spacing: 0.3px;">
566
- <div style="color: #d1d5db; font-weight: 500;"><strong style="color: #f3f4f6; font-weight: 600;">Generated:</strong> ${new Date().toLocaleDateString(
567
- 'en-US',
568
- {
569
- weekday: 'long',
570
- year: 'numeric',
571
- month: 'long',
572
- day: 'numeric',
573
- }
574
- )}</div>
575
- <div style="color: #d1d5db; font-weight: 500;"><strong style="color: #f3f4f6; font-weight: 600;">Time:</strong> ${new Date().toLocaleTimeString()}</div>
576
- </div>
577
- </div>
578
-
579
-
580
- ${cameras
581
- .map((camera, index) => {
582
- const verkadaCamera = camera.verkadaModel
583
- ? getCameraById(camera.verkadaModel)
584
- : null;
585
- // Generate sequential camera number
586
- const cameraNumber = index + 1;
587
-
588
- return `
589
- <div style="margin-bottom: 20px; background: white; border: 1px solid #d1d5db; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.08); page-break-inside: avoid;">
590
- <!-- Camera Header -->
591
- <div style="background: ${
592
- verkadaCamera?.environment === 'indoor'
593
- ? 'linear-gradient(135deg, #0d9488 0%, #0f766e 100%)'
594
- : 'linear-gradient(135deg, #ea580c 0%, #dc2626 100%)'
595
- }; color: white; padding: 16px 20px; position: relative;">
596
- <div style="display: flex; align-items: center; gap: 16px;">
597
- <div style="width: 40px; height: 40px; background: rgba(255,255,255,0.15); border: 2px solid rgba(255,255,255,0.25); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 16px; flex-shrink: 0; text-shadow: 0 1px 3px rgba(0,0,0,0.3);">${cameraNumber}</div>
598
- <div style="flex: 1;">
599
- <h4 style="margin: 0; font-size: 18px; font-weight: 700; letter-spacing: -0.3px; text-shadow: 0 1px 3px rgba(0,0,0,0.2);">Camera ${cameraNumber}</h4>
600
- <div style="font-size: 13px; opacity: 0.95; margin-top: 4px; letter-spacing: 0.2px;">${
601
- verkadaCamera
602
- ? verkadaCamera.name
603
- : 'Model Not Selected'
604
- }</div>
605
- </div>
606
- ${
607
- verkadaCamera?.ptz
608
- ? '<div style="background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); padding: 6px 12px; border-radius: 16px; font-size: 11px; font-weight: 600; letter-spacing: 0.5px;">PTZ</div>'
609
- : ''
610
- }
611
- </div>
612
- </div>
613
-
614
- <!-- Camera Details -->
615
- <div style="padding: 20px;">
616
- ${
617
- verkadaCamera
618
- ? `
619
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
620
- <div style="background: #f8fafc; padding: 12px; border-radius: 10px; border-left: 3px solid #3b82f6;">
621
- <div style="font-size: 12px; color: #475569; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 4px;">Resolution</div>
622
- <div style="font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.3;">${
623
- verkadaCamera.resolution
624
- }</div>
625
- </div>
626
- <div style="background: #f8fafc; padding: 12px; border-radius: 10px; border-left: 3px solid #8b5cf6;">
627
- <div style="font-size: 12px; color: #475569; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 4px;">Field of View</div>
628
- <div style="font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.3;">${
629
- verkadaCamera.fov
630
- }°</div>
631
- </div>
632
- <div style="background: #f8fafc; padding: 12px; border-radius: 10px; border-left: 3px solid ${
633
- verkadaCamera.environment ===
634
- 'indoor'
635
- ? '#10b981'
636
- : '#f59e0b'
637
- };">
638
- <div style="font-size: 12px; color: #475569; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 4px;">Environment</div>
639
- <div style="font-size: 15px; font-weight: 700; color: #0f172a; text-transform: capitalize; line-height: 1.3;">${
640
- verkadaCamera.environment
641
- }</div>
642
- </div>
643
- <div style="background: #f8fafc; padding: 12px; border-radius: 10px; border-left: 3px solid #ef4444;">
644
- <div style="font-size: 12px; color: #475569; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 4px;">IR Range</div>
645
- <div style="font-size: 15px; font-weight: 700; color: #0f172a; line-height: 1.3;">${
646
- verkadaCamera.irRange
647
- }m</div>
648
- </div>
649
- </div>
650
-
651
- <!-- Position & Direction -->
652
- <div style="background: linear-gradient(135deg, #fefefe 0%, #f1f5f9 100%); border: 1px solid #e2e8f0; padding: 16px; border-radius: 12px; margin-bottom: 16px;">
653
- <div style="font-size: 13px; color: #334155; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 12px; text-align: center; border-bottom: 1px solid #e2e8f0; padding-bottom: 8px;">Installation Details</div>
654
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
655
- <div style="background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0;">
656
- <div style="font-size: 11px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Direction</div>
657
- <div style="font-size: 14px; font-weight: 600; color: #0f172a; line-height: 1.4;">${
658
- camera.direction
659
- ? `${camera.direction}° from North`
660
- : 'Not set'
661
- }</div>
662
- </div>
663
- <div style="background: white; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0;">
664
- <div style="font-size: 11px; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Coordinates</div>
665
- <div style="font-size: 14px; font-weight: 600; color: #0f172a; line-height: 1.4; font-family: monospace;">${
666
- mode === 'map'
667
- ? `${camera.lat?.toFixed(
668
- 6
669
- )}, ${camera.lng?.toFixed(
670
- 6
671
- )}`
672
- : `X: ${Math.round(
673
- camera.x
674
- )}, Y: ${Math.round(
675
- camera.y
676
- )}`
677
- }</div>
678
- </div>
679
- </div>
680
- </div>
681
-
682
- <!-- Features -->
683
- ${
684
- verkadaCamera.features &&
685
- verkadaCamera.features.length > 0
686
- ? `
687
- <div style="background: white; padding: 14px; border-radius: 12px; border: 1px solid #e2e8f0;">
688
- <div style="font-size: 13px; color: #334155; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 10px; text-align: center;">Key Features</div>
689
- <div style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;">
690
- ${verkadaCamera.features
691
- .slice(0, 6)
692
- .map(
693
- (feature) =>
694
- `<span style="background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); color: #1e40af; border: 1px solid #93c5fd; padding: 6px 12px; border-radius: 18px; font-size: 11px; font-weight: 600; letter-spacing: 0.3px; white-space: nowrap;">${feature}</span>`
695
- )
696
- .join('')}
697
- ${
698
- verkadaCamera.features
699
- .length > 6
700
- ? `<span style="color: #64748b; font-size: 11px; align-self: center; padding: 4px 8px; border-radius: 12px; background: #f1f5f9; font-weight: 500;">+${
701
- verkadaCamera
702
- .features
703
- .length - 6
704
- } more</span>`
705
- : ''
706
- }
707
- </div>
708
- </div>
709
- `
710
- : ''
711
- }
712
- `
713
- : `
714
- <div style="text-align: center; padding: 30px; background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); border: 1px solid #fca5a5; border-radius: 12px; margin: 10px 0;">
715
- <div style="font-size: 16px; font-weight: 700; margin-bottom: 8px; color: #dc2626; letter-spacing: 0.2px;">⚠ No Camera Model Selected</div>
716
- <div style="font-size: 13px; color: #7f1d1d; line-height: 1.5; letter-spacing: 0.2px;">Please configure this camera before installation</div>
717
- </div>
718
- `
719
- }
720
- </div>
721
- </div>
722
- `;
723
- })
724
- .join('')}
725
-
726
- <!-- Report Footer -->
727
- <div style="margin-top: 32px; padding: 24px; background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); border-radius: 16px; border: 1px solid #cbd5e1; box-shadow: 0 4px 12px rgba(0,0,0,0.08);">
728
- <!-- Legend Section -->
729
- <div style="margin-bottom: 24px;">
730
- <h3 style="margin: 0 0 16px 0; font-size: 20px; font-weight: 800; color: #0f172a; text-align: center; letter-spacing: -0.4px;">Camera Types & Legend</h3>
731
- <div style="display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; margin-bottom: 20px;">
732
- <div style="display: flex; align-items: center; gap: 12px; background: white; padding: 12px 16px; border-radius: 12px; border: 1px solid #cbd5e1; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
733
- <div style="width: 24px; height: 24px; background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%); border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 6px rgba(13,148,136,0.3);"></div>
734
- <span style="font-size: 13px; font-weight: 600; color: #1e293b; letter-spacing: 0.2px;">Indoor Camera</span>
735
- </div>
736
- <div style="display: flex; align-items: center; gap: 12px; background: white; padding: 12px 16px; border-radius: 12px; border: 1px solid #cbd5e1; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
737
- <div style="width: 24px; height: 24px; background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%); border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 6px rgba(234,88,12,0.3);"></div>
738
- <span style="font-size: 13px; font-weight: 600; color: #1e293b; letter-spacing: 0.2px;">Outdoor Camera</span>
739
- </div>
740
- ${
741
- cameras.some(
742
- (c) => getCameraById(c.verkadaModel)?.ptz
743
- )
744
- ? `
745
- <div style="display: flex; align-items: center; gap: 12px; background: white; padding: 12px 16px; border-radius: 12px; border: 1px solid #cbd5e1; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
746
- <div style="width: 24px; height: 24px; background: #6b7280; border-radius: 50%; border: 3px solid #ef4444; box-shadow: 0 2px 6px rgba(239,68,68,0.3);"></div>
747
- <span style="font-size: 13px; font-weight: 600; color: #1e293b; letter-spacing: 0.2px;">PTZ Camera</span>
748
- </div>`
749
- : ''
750
- }
751
- </div>
752
- </div>
753
-
754
- <!-- Technical Notes -->
755
- <div style="background: white; padding: 20px; border-radius: 12px; border: 1px solid #cbd5e1; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
756
- <h4 style="margin: 0 0 16px 0; font-size: 16px; font-weight: 700; color: #0f172a; text-align: center; letter-spacing: -0.2px;">Installation Notes</h4>
757
- <div style="font-size: 13px; line-height: 1.8; color: #374151; letter-spacing: 0.2px;">
758
- <div style="margin-bottom: 12px; padding: 10px; background: #f8fafc; border-radius: 8px; border-left: 3px solid #3b82f6;"><strong style="color: #1e40af;">Direction Reference:</strong> All camera directions are measured clockwise from North (0° = North, 90° = East, 180° = South, 270° = West)</div>
759
- <div style="margin-bottom: 12px; padding: 10px; background: #f8fafc; border-radius: 8px; border-left: 3px solid #8b5cf6;"><strong style="color: #7c3aed;">Field of View:</strong> FOV angles represent the horizontal viewing angle. Actual coverage may vary based on mounting height and obstacles</div>
760
- <div style="margin-bottom: 12px; padding: 10px; background: #f8fafc; border-radius: 8px; border-left: 3px solid #10b981;"><strong style="color: #059669;">Coordinates:</strong> ${
761
- mode === 'map'
762
- ? 'Geographic coordinates (Latitude, Longitude) in decimal degrees'
763
- : 'Pixel coordinates relative to uploaded image (X, Y from top-left corner)'
764
- }</div>
765
- <div style="padding: 12px; background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); border-radius: 8px; border-left: 3px solid #ef4444;"><strong style="color: #dc2626;">Important:</strong> This layout should be verified on-site during installation. Actual camera placement may need adjustment based on physical constraints, lighting conditions, and site requirements</div>
766
- </div>
767
- </div>
768
-
769
- <!-- Report ID & Disclaimer -->
770
- <div style="text-align: center; margin-top: 20px; padding-top: 16px; border-top: 2px solid #cbd5e1;">
771
- <div style="font-size: 12px; color: #475569; margin-bottom: 8px; letter-spacing: 0.5px; font-weight: 600;">Report ID: <span style="font-family: monospace; background: #f1f5f9; padding: 2px 6px; border-radius: 4px;">${Date.now()
772
- .toString(36)
773
- .toUpperCase()}</span></div>
774
- <div style="font-size: 11px; color: #64748b; line-height: 1.6; letter-spacing: 0.3px; max-width: 400px; margin: 0 auto;">This report was generated automatically and should be reviewed by qualified security installation professionals</div>
775
- </div>
776
- </div>
777
- `;
486
+ let mapImageData = null;
778
487
 
779
- cameraListDiv.innerHTML = cameraListHTML;
780
- exportContainer.appendChild(cameraListDiv);
781
-
782
- // Wait for rendering
783
- await new Promise((resolve) => setTimeout(resolve, 100));
784
-
785
- // Capture the composite with exact dimensions
786
- const finalCanvas = await html2canvas(exportContainer, {
787
- useCORS: true,
788
- allowTaint: true,
789
- scale: 1,
790
- backgroundColor: '#ffffff',
791
- logging: false,
792
- width: finalWidth,
793
- height: finalHeight,
794
- });
488
+ // Capture the site map image if available
489
+ if (uploadedImage || mode === 'image') {
490
+ const imageContainer = document.querySelector(
491
+ `.${styles.imageContainer}`
492
+ );
493
+ if (imageContainer && uploadedImage) {
494
+ try {
495
+ const canvas = await html2canvas(imageContainer, {
496
+ useCORS: true,
497
+ allowTaint: true,
498
+ scale: 1.2,
499
+ backgroundColor: '#f8fafc',
500
+ logging: false,
501
+ });
502
+ mapImageData = canvas.toDataURL('image/png', 0.9);
503
+ } catch (error) {
504
+ console.warn('Could not capture site map image:', error);
505
+ // Continue without image if capture fails
506
+ }
507
+ }
508
+ }
795
509
 
796
- // Clean up
797
- document.body.removeChild(exportContainer);
510
+ // Generate PDF using React-PDF with fallback
511
+ let pdfBlob;
512
+ try {
513
+ // Try the enhanced PDF first
514
+ pdfBlob = await pdf(
515
+ React.createElement(CameraReportPDF, {
516
+ cameras: cameras,
517
+ mapImageData: mapImageData,
518
+ mode: mode,
519
+ customColors: {} // Can be extended to read from props or CSS variables
520
+ })
521
+ ).toBlob();
522
+ } catch (pdfError) {
523
+ console.warn('Enhanced PDF failed, trying simple version:', pdfError);
524
+
525
+ // Fallback to simple PDF
526
+ pdfBlob = await pdf(
527
+ React.createElement(SimpleCameraReportPDF, {
528
+ cameras: cameras,
529
+ mapImageData: mapImageData,
530
+ mode: mode,
531
+ customColors: {} // Can be extended to read from props or CSS variables
532
+ })
533
+ ).toBlob();
534
+ }
798
535
 
799
- // Download
536
+ // Create download link
537
+ const url = URL.createObjectURL(pdfBlob);
800
538
  const link = document.createElement('a');
801
539
  const timestamp = new Date()
802
540
  .toISOString()
803
541
  .slice(0, 19)
804
542
  .replace(/[:.]/g, '-');
805
- link.download = `Security-Camera-Installation-Report-${timestamp}.png`;
806
- link.href = finalCanvas.toDataURL('image/png', 1.0); // Maximum quality
543
+ link.download = `Security-Camera-Installation-Report-${timestamp}.pdf`;
544
+ link.href = url;
807
545
  link.click();
808
546
 
547
+ // Clean up
548
+ URL.revokeObjectURL(url);
549
+
809
550
  // Show success message
810
551
  Swal.fire({
811
552
  icon: 'success',
812
- title: 'Export Complete!',
813
- text: `Professional camera installation report exported as ${link.download}`,
814
- timer: 3000,
815
- showConfirmButton: false,
816
- toast: true,
817
- position: 'top-end',
553
+ title: 'PDF Export Complete!',
554
+ html: `
555
+ <div style="text-align: left; font-size: 14px; line-height: 1.6;">
556
+ <p><strong>Your professional camera installation report has been generated!</strong></p>
557
+ <div style="margin: 15px 0;">
558
+ <div style="display: flex; align-items: center; margin-bottom: 8px;">
559
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; flex-shrink: 0;">
560
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
561
+ <polyline points="14,2 14,8 20,8"></polyline>
562
+ </svg>
563
+ <span><strong>Page 1:</strong> Site layout with camera positions</span>
564
+ </div>
565
+ <div style="display: flex; align-items: center; margin-bottom: 8px;">
566
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; flex-shrink: 0;">
567
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
568
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
569
+ <polyline points="21,15 16,10 5,21"></polyline>
570
+ </svg>
571
+ <span><strong>Page 2:</strong> Detailed camera specifications</span>
572
+ </div>
573
+ <div style="display: flex; align-items: center;">
574
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px; flex-shrink: 0;">
575
+ <polyline points="20,6 9,17 4,12"></polyline>
576
+ </svg>
577
+ <span><strong>Includes:</strong> Camera legend and specifications</span>
578
+ </div>
579
+ </div>
580
+ <p style="color: #6b7280; font-size: 12px; margin-top: 15px;">
581
+ File saved as: <code>${link.download}</code>
582
+ </p>
583
+ </div>
584
+ `,
585
+ confirmButtonColor: '#1e40af',
586
+ confirmButtonText: 'Great!',
818
587
  });
819
588
  } catch (error) {
820
- console.error('Error exporting:', error);
589
+ console.error('Error generating PDF:', error);
590
+
591
+ // More specific error handling
592
+ let errorMessage = 'Unable to generate PDF report. Please try again.';
593
+ if (error.message.includes('Buffer') || error.message.includes('DataView')) {
594
+ errorMessage = 'PDF generation failed due to font loading issue. This is a known issue with React-PDF in development mode.';
595
+ } else if (error.message.includes('font') || error.message.includes('glyph')) {
596
+ errorMessage = 'Font-related error occurred during PDF generation. Trying alternative approach...';
597
+ }
598
+
821
599
  Swal.fire({
822
600
  icon: 'error',
823
- title: 'Export Failed',
824
- text: 'Export failed. Please try again or take a manual screenshot.',
825
- confirmButtonColor: '#10b981',
601
+ title: 'PDF Export Failed',
602
+ html: `
603
+ <div style="text-align: left;">
604
+ <p>${errorMessage}</p>
605
+ <details style="margin-top: 10px;">
606
+ <summary style="cursor: pointer; color: #6b7280;">Technical Details</summary>
607
+ <pre style="font-size: 11px; background: #f3f4f6; padding: 8px; margin: 8px 0; border-radius: 4px; max-height: 100px; overflow-y: auto;">${error.message}</pre>
608
+ </details>
609
+ </div>
610
+ `,
611
+ confirmButtonColor: '#dc2626',
826
612
  });
827
613
  }
828
- }, [cameras]);
614
+ }, [cameras, uploadedImage, mode]);
829
615
 
830
616
  // Update cameras with markers on map
831
617
  useEffect(() => {
@@ -1145,11 +931,9 @@ const CameraPlacement = () => {
1145
931
  />
1146
932
  <IconButton
1147
933
  icon={Download}
1148
- onClick={exportMap}
1149
- disabled={mode === 'image' && !uploadedImage}
1150
- tooltip={`Export ${
1151
- mode === 'map' ? 'Map' : 'Image'
1152
- }`}
934
+ onClick={exportToPDF}
935
+ disabled={!cameras.length}
936
+ tooltip="Export to PDF Report"
1153
937
  className={styles.exportBtn}
1154
938
  />
1155
939
  </div>