@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
|
|
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: '#
|
|
468
|
+
confirmButtonColor: '#1e40af',
|
|
466
469
|
});
|
|
467
470
|
return;
|
|
468
471
|
}
|
|
469
472
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
//
|
|
797
|
-
|
|
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
|
-
//
|
|
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}.
|
|
806
|
-
link.href =
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
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
|
-
|
|
825
|
-
|
|
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={
|
|
1149
|
-
disabled={
|
|
1150
|
-
tooltip=
|
|
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>
|