@visns-studio/visns-components 5.15.7 → 5.15.9

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,625 @@
1
+ import React from 'react';
2
+ import {
3
+ Document,
4
+ Page,
5
+ View,
6
+ Text,
7
+ Image,
8
+ StyleSheet,
9
+ } from '@react-pdf/renderer';
10
+ import { getCameraById } from '../data/verkadaCameras';
11
+
12
+ // Default neutral professional color theme
13
+ const defaultColors = {
14
+ primary: '#2d3748', // Professional dark gray
15
+ secondary: '#4a5568', // Medium gray
16
+ background: '#f7fafc', // Light gray background
17
+ paragraph: '#4a5568', // Medium gray text
18
+ white: '#ffffff',
19
+ border: '#e2e8f0',
20
+ accent: '#3182ce', // Professional blue accent
21
+ };
22
+
23
+ // Function to get colors - prioritize neutral professional colors over CSS variables
24
+ const getColors = (customColors = {}) => {
25
+ // Use the neutral professional colors as the primary choice
26
+ return {
27
+ ...defaultColors,
28
+ ...customColors, // Allow custom override if needed
29
+ };
30
+ };
31
+
32
+ // Function to create styles with dynamic colors
33
+ const createStyles = (colors) => StyleSheet.create({
34
+ // Page styles
35
+ page: {
36
+ fontFamily: 'Helvetica',
37
+ fontSize: 9,
38
+ padding: 15,
39
+ backgroundColor: '#ffffff',
40
+ },
41
+ landscapePage: {
42
+ fontFamily: 'Helvetica',
43
+ fontSize: 10,
44
+ padding: 15,
45
+ backgroundColor: '#ffffff',
46
+ },
47
+
48
+ // Compact header styles for site map page
49
+ compactHeader: {
50
+ backgroundColor: colors.primary,
51
+ color: colors.white,
52
+ padding: 8,
53
+ marginBottom: 8,
54
+ textAlign: 'center',
55
+ },
56
+ compactHeaderTitle: {
57
+ fontSize: 16,
58
+ fontWeight: 700,
59
+ margin: 0,
60
+ },
61
+
62
+ // Full header styles for details page
63
+ header: {
64
+ backgroundColor: colors.primary,
65
+ color: colors.white,
66
+ padding: 15,
67
+ marginBottom: 15,
68
+ borderRadius: 0,
69
+ },
70
+ headerTitle: {
71
+ fontSize: 20,
72
+ fontWeight: 700,
73
+ textAlign: 'center',
74
+ marginBottom: 5,
75
+ },
76
+ headerSubtitle: {
77
+ fontSize: 10,
78
+ textAlign: 'center',
79
+ opacity: 0.9,
80
+ marginBottom: 8,
81
+ },
82
+ headerInfo: {
83
+ flexDirection: 'row',
84
+ justifyContent: 'space-between',
85
+ fontSize: 8,
86
+ borderTop: '1px solid rgba(255,255,255,0.2)',
87
+ paddingTop: 8,
88
+ },
89
+
90
+ // Map page styles - leave room for header text
91
+ mapContainer: {
92
+ flex: 1,
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ backgroundColor: colors.background,
96
+ borderRadius: 4,
97
+ border: `1px solid ${colors.border}`,
98
+ minHeight: '88%',
99
+ },
100
+ mapImage: {
101
+ maxWidth: '100%',
102
+ maxHeight: '100%',
103
+ objectFit: 'contain',
104
+ },
105
+ mapPlaceholder: {
106
+ padding: 40,
107
+ textAlign: 'center',
108
+ color: '#6b7280',
109
+ },
110
+
111
+ // Camera details styles - ultra compact
112
+ detailsHeader: {
113
+ fontSize: 12,
114
+ fontWeight: 700,
115
+ color: colors.primary,
116
+ marginBottom: 8,
117
+ textAlign: 'center',
118
+ },
119
+ cameraCard: {
120
+ marginBottom: 6,
121
+ border: `1px solid ${colors.border}`,
122
+ borderRadius: 3,
123
+ overflow: 'hidden',
124
+ backgroundColor: colors.white,
125
+ },
126
+ cameraHeader: {
127
+ backgroundColor: colors.primary,
128
+ color: colors.white,
129
+ padding: 6,
130
+ flexDirection: 'row',
131
+ alignItems: 'center',
132
+ },
133
+ cameraNumber: {
134
+ width: 24,
135
+ height: 24,
136
+ backgroundColor: 'rgba(255,255,255,0.2)',
137
+ borderRadius: 12,
138
+ alignItems: 'center',
139
+ justifyContent: 'center',
140
+ marginRight: 8,
141
+ border: '1px solid rgba(255,255,255,0.3)',
142
+ },
143
+ cameraNumberText: {
144
+ fontSize: 11,
145
+ fontWeight: 700,
146
+ color: colors.white,
147
+ },
148
+ cameraTitle: {
149
+ flex: 1,
150
+ },
151
+ cameraTitleText: {
152
+ fontSize: 12,
153
+ fontWeight: 700,
154
+ color: colors.white,
155
+ marginBottom: 1,
156
+ },
157
+ cameraModelText: {
158
+ fontSize: 9,
159
+ color: colors.white,
160
+ opacity: 0.9,
161
+ },
162
+ ptzBadge: {
163
+ backgroundColor: 'rgba(255,255,255,0.2)',
164
+ border: '1px solid rgba(255,255,255,0.3)',
165
+ borderRadius: 12,
166
+ paddingHorizontal: 8,
167
+ paddingVertical: 4,
168
+ },
169
+ ptzText: {
170
+ fontSize: 10,
171
+ fontWeight: 600,
172
+ color: colors.white,
173
+ },
174
+ environmentIndicator: {
175
+ width: 12,
176
+ height: 12,
177
+ borderRadius: 6,
178
+ marginLeft: 8,
179
+ border: '2px solid rgba(255,255,255,0.8)',
180
+ },
181
+ environmentIndoorIndicator: {
182
+ backgroundColor: '#22c55e', // Green for indoor
183
+ },
184
+ environmentOutdoorIndicator: {
185
+ backgroundColor: '#dc2626', // Red for outdoor
186
+ },
187
+
188
+ // Camera details content - ultra compact
189
+ cameraContent: {
190
+ padding: 4,
191
+ flexDirection: 'row',
192
+ gap: 6,
193
+ },
194
+ cameraImageContainer: {
195
+ width: 60,
196
+ height: 45,
197
+ backgroundColor: colors.background,
198
+ borderRadius: 3,
199
+ border: `1px solid ${colors.border}`,
200
+ alignItems: 'center',
201
+ justifyContent: 'center',
202
+ flexShrink: 0,
203
+ },
204
+ cameraImage: {
205
+ maxWidth: '100%',
206
+ maxHeight: '100%',
207
+ objectFit: 'contain',
208
+ },
209
+ cameraImagePlaceholder: {
210
+ fontSize: 8,
211
+ color: colors.paragraph,
212
+ textAlign: 'center',
213
+ },
214
+ cameraDetailsContainer: {
215
+ flex: 1,
216
+ },
217
+ specsGrid: {
218
+ flexDirection: 'row',
219
+ flexWrap: 'wrap',
220
+ marginBottom: 4,
221
+ gap: 3,
222
+ },
223
+ specItem: {
224
+ backgroundColor: colors.background,
225
+ padding: 3,
226
+ borderRadius: 2,
227
+ borderLeft: `2px solid ${colors.accent}`,
228
+ width: '48%',
229
+ },
230
+ specLabel: {
231
+ fontSize: 6,
232
+ color: colors.paragraph,
233
+ fontWeight: 600,
234
+ textTransform: 'uppercase',
235
+ marginBottom: 1,
236
+ },
237
+ specValue: {
238
+ fontSize: 8,
239
+ color: colors.primary,
240
+ fontWeight: 600,
241
+ },
242
+ installationDetails: {
243
+ backgroundColor: colors.white,
244
+ border: `1px solid ${colors.primary}`,
245
+ borderRadius: 3,
246
+ padding: 4,
247
+ marginBottom: 4,
248
+ },
249
+ installationTitle: {
250
+ fontSize: 8,
251
+ color: colors.paragraph,
252
+ fontWeight: 600,
253
+ textTransform: 'uppercase',
254
+ textAlign: 'center',
255
+ marginBottom: 4,
256
+ },
257
+ coordinatesGrid: {
258
+ flexDirection: 'row',
259
+ gap: 4,
260
+ },
261
+ coordinateItem: {
262
+ backgroundColor: colors.background,
263
+ padding: 3,
264
+ borderRadius: 2,
265
+ border: `1px solid ${colors.secondary}`,
266
+ flex: 1,
267
+ },
268
+ coordinateLabel: {
269
+ fontSize: 7,
270
+ color: colors.paragraph,
271
+ fontWeight: 600,
272
+ textTransform: 'uppercase',
273
+ marginBottom: 1,
274
+ },
275
+ coordinateValue: {
276
+ fontSize: 8,
277
+ color: colors.primary,
278
+ fontWeight: 600,
279
+ fontFamily: 'Helvetica',
280
+ },
281
+ featuresContainer: {
282
+ backgroundColor: colors.background,
283
+ padding: 3,
284
+ borderRadius: 2,
285
+ border: `1px solid ${colors.border}`,
286
+ },
287
+ featuresTitle: {
288
+ fontSize: 7,
289
+ color: colors.paragraph,
290
+ fontWeight: 600,
291
+ textTransform: 'uppercase',
292
+ textAlign: 'center',
293
+ marginBottom: 3,
294
+ },
295
+ featuresGrid: {
296
+ flexDirection: 'row',
297
+ flexWrap: 'wrap',
298
+ gap: 2,
299
+ justifyContent: 'center',
300
+ },
301
+ featureTag: {
302
+ backgroundColor: colors.white,
303
+ color: colors.accent,
304
+ border: `1px solid ${colors.accent}`,
305
+ borderRadius: 8,
306
+ paddingHorizontal: 4,
307
+ paddingVertical: 2,
308
+ fontSize: 7,
309
+ fontWeight: 600,
310
+ },
311
+ noModelWarning: {
312
+ backgroundColor: '#fef2f2',
313
+ border: '1px solid #fca5a5',
314
+ borderRadius: 8,
315
+ padding: 16,
316
+ textAlign: 'center',
317
+ margin: '8px 0',
318
+ },
319
+ warningTitle: {
320
+ fontSize: 12,
321
+ fontWeight: 600,
322
+ color: '#dc2626',
323
+ marginBottom: 4,
324
+ },
325
+ warningText: {
326
+ fontSize: 10,
327
+ color: '#7f1d1d',
328
+ },
329
+
330
+ // Footer styles
331
+ footer: {
332
+ marginTop: 8,
333
+ padding: 6,
334
+ backgroundColor: colors.background,
335
+ borderRadius: 3,
336
+ border: `1px solid ${colors.border}`,
337
+ },
338
+ footerTitle: {
339
+ fontSize: 9,
340
+ fontWeight: 700,
341
+ color: colors.primary,
342
+ textAlign: 'center',
343
+ marginBottom: 4,
344
+ },
345
+ legendContainer: {
346
+ flexDirection: 'row',
347
+ justifyContent: 'center',
348
+ gap: 6,
349
+ marginBottom: 4,
350
+ },
351
+ legendItem: {
352
+ flexDirection: 'row',
353
+ alignItems: 'center',
354
+ gap: 3,
355
+ backgroundColor: colors.white,
356
+ padding: '3px 5px',
357
+ borderRadius: 2,
358
+ border: `1px solid ${colors.border}`,
359
+ },
360
+ legendDot: {
361
+ width: 10,
362
+ height: 10,
363
+ borderRadius: 5,
364
+ border: `1px solid ${colors.white}`,
365
+ },
366
+ legendDotIndoor: {
367
+ backgroundColor: '#22c55e', // Green for indoor
368
+ },
369
+ legendDotOutdoor: {
370
+ backgroundColor: '#dc2626', // Red for outdoor
371
+ },
372
+ legendDotPtz: {
373
+ backgroundColor: '#6b7280',
374
+ border: '2px solid #ef4444',
375
+ },
376
+ legendText: {
377
+ fontSize: 7,
378
+ color: colors.paragraph,
379
+ fontWeight: 600,
380
+ },
381
+ notesContainer: {
382
+ backgroundColor: colors.white,
383
+ padding: 6,
384
+ borderRadius: 3,
385
+ border: `1px solid ${colors.border}`,
386
+ },
387
+ notesTitle: {
388
+ fontSize: 9,
389
+ fontWeight: 700,
390
+ color: colors.primary,
391
+ textAlign: 'center',
392
+ marginBottom: 4,
393
+ },
394
+ noteItem: {
395
+ flexDirection: 'row',
396
+ marginBottom: 3,
397
+ backgroundColor: colors.white,
398
+ padding: 3,
399
+ borderRadius: 2,
400
+ borderLeft: `2px solid ${colors.secondary}`,
401
+ },
402
+ noteText: {
403
+ fontSize: 7,
404
+ color: colors.paragraph,
405
+ lineHeight: 1.3,
406
+ flex: 1,
407
+ },
408
+ disclaimer: {
409
+ textAlign: 'center',
410
+ marginTop: 4,
411
+ paddingTop: 3,
412
+ borderTop: `1px solid ${colors.border}`,
413
+ },
414
+ disclaimerText: {
415
+ fontSize: 6,
416
+ color: colors.paragraph,
417
+ },
418
+ reportId: {
419
+ fontSize: 6,
420
+ color: colors.paragraph,
421
+ marginBottom: 2,
422
+ },
423
+ });
424
+
425
+ // Camera Card Component
426
+ const CameraCard = ({ camera, number, mode, styles }) => {
427
+ const verkadaCamera = camera.verkadaModel
428
+ ? getCameraById(camera.verkadaModel)
429
+ : null;
430
+
431
+ return (
432
+ <View style={styles.cameraCard}>
433
+ {/* Camera Header */}
434
+ <View style={styles.cameraHeader}>
435
+ <View style={styles.cameraNumber}>
436
+ <Text style={styles.cameraNumberText}>{number}</Text>
437
+ </View>
438
+ <View style={styles.cameraTitle}>
439
+ <Text style={styles.cameraTitleText}>
440
+ Camera {number}
441
+ </Text>
442
+ <Text style={styles.cameraModelText}>
443
+ {verkadaCamera ? verkadaCamera.name : 'Model Not Selected'}
444
+ </Text>
445
+ </View>
446
+ {verkadaCamera?.environment && (
447
+ <View
448
+ style={[
449
+ styles.environmentIndicator,
450
+ verkadaCamera.environment === 'indoor'
451
+ ? styles.environmentIndoorIndicator
452
+ : styles.environmentOutdoorIndicator,
453
+ ]}
454
+ />
455
+ )}
456
+ {verkadaCamera?.ptz && (
457
+ <View style={styles.ptzBadge}>
458
+ <Text style={styles.ptzText}>PTZ</Text>
459
+ </View>
460
+ )}
461
+ </View>
462
+
463
+ {/* Camera Content */}
464
+ <View style={styles.cameraContent}>
465
+ {/* Camera Image */}
466
+ <View style={styles.cameraImageContainer}>
467
+ {verkadaCamera?.image ? (
468
+ <Image src={verkadaCamera.image} style={styles.cameraImage} />
469
+ ) : (
470
+ <Text style={styles.cameraImagePlaceholder}>
471
+ No Image{'\n'}Available
472
+ </Text>
473
+ )}
474
+ </View>
475
+
476
+ {/* Camera Details */}
477
+ <View style={styles.cameraDetailsContainer}>
478
+ {verkadaCamera ? (
479
+ <View>
480
+ {/* Specifications Grid */}
481
+ <View style={styles.specsGrid}>
482
+ <View style={styles.specItem}>
483
+ <Text style={styles.specLabel}>Resolution</Text>
484
+ <Text style={styles.specValue}>
485
+ {verkadaCamera.resolution}
486
+ </Text>
487
+ </View>
488
+ <View style={styles.specItem}>
489
+ <Text style={styles.specLabel}>Field of View</Text>
490
+ <Text style={styles.specValue}>
491
+ {verkadaCamera.fov}°
492
+ </Text>
493
+ </View>
494
+ <View style={styles.specItem}>
495
+ <Text style={styles.specLabel}>Environment</Text>
496
+ <Text style={styles.specValue}>
497
+ {verkadaCamera.environment}
498
+ </Text>
499
+ </View>
500
+ <View style={styles.specItem}>
501
+ <Text style={styles.specLabel}>IR Range</Text>
502
+ <Text style={styles.specValue}>
503
+ {verkadaCamera.irRange}m
504
+ </Text>
505
+ </View>
506
+ </View>
507
+
508
+ {/* Features */}
509
+ {verkadaCamera.features && verkadaCamera.features.length > 0 && (
510
+ <View style={styles.featuresContainer}>
511
+ <Text style={styles.featuresTitle}>Key Features</Text>
512
+ <View style={styles.featuresGrid}>
513
+ {verkadaCamera.features.slice(0, 6).map((feature, index) => (
514
+ <View key={index} style={styles.featureTag}>
515
+ <Text>{feature}</Text>
516
+ </View>
517
+ ))}
518
+ {verkadaCamera.features.length > 6 && (
519
+ <View style={styles.featureTag}>
520
+ <Text>+{verkadaCamera.features.length - 6} more</Text>
521
+ </View>
522
+ )}
523
+ </View>
524
+ </View>
525
+ )}
526
+ </View>
527
+ ) : (
528
+ <View style={styles.noModelWarning}>
529
+ <Text style={styles.warningTitle}>⚠ No Camera Model Selected</Text>
530
+ <Text style={styles.warningText}>
531
+ Please configure this camera before installation
532
+ </Text>
533
+ </View>
534
+ )}
535
+ </View>
536
+ </View>
537
+ </View>
538
+ );
539
+ };
540
+
541
+ // Main PDF Document Component
542
+ const CameraReportPDF = ({ cameras, mapImageData, mode, customColors }) => {
543
+ const currentDate = new Date();
544
+ const reportId = Date.now().toString(36).toUpperCase();
545
+ const colors = getColors(customColors);
546
+ const styles = createStyles(colors);
547
+
548
+ return (
549
+ <Document
550
+ title="Security Camera Installation Report"
551
+ author="VISNS Studio"
552
+ subject="Camera Installation Layout and Specifications"
553
+ creator="VISNS Components"
554
+ >
555
+ {/* Page 1: Site Map */}
556
+ <Page size="A4" orientation="landscape" style={styles.landscapePage}>
557
+ {/* Compact Header */}
558
+ <View style={styles.compactHeader}>
559
+ <Text style={styles.compactHeaderTitle}>
560
+ Site Map Layout
561
+ </Text>
562
+ </View>
563
+
564
+ {/* Map Container - maximized */}
565
+ <View style={styles.mapContainer}>
566
+ {mapImageData ? (
567
+ <Image src={mapImageData} style={styles.mapImage} />
568
+ ) : (
569
+ <View style={styles.mapPlaceholder}>
570
+ <Text>Site map will be displayed here</Text>
571
+ <Text>Please upload an image to include the site layout</Text>
572
+ </View>
573
+ )}
574
+ </View>
575
+ </Page>
576
+
577
+ {/* Page 2+: Camera Details */}
578
+ <Page size="A4" style={styles.page}>
579
+ <Text style={styles.detailsHeader}>Camera Specifications & Details</Text>
580
+
581
+ {cameras.map((camera, index) => (
582
+ <CameraCard
583
+ key={camera.id}
584
+ camera={camera}
585
+ number={index + 1}
586
+ mode={mode}
587
+ styles={styles}
588
+ />
589
+ ))}
590
+
591
+ {/* Footer with Legend */}
592
+ <View style={styles.footer}>
593
+ <Text style={styles.footerTitle}>Camera Legend</Text>
594
+
595
+ {/* Legend */}
596
+ <View style={styles.legendContainer}>
597
+ <View style={styles.legendItem}>
598
+ <View style={[styles.legendDot, styles.legendDotIndoor]} />
599
+ <Text style={styles.legendText}>Indoor</Text>
600
+ </View>
601
+ <View style={styles.legendItem}>
602
+ <View style={[styles.legendDot, styles.legendDotOutdoor]} />
603
+ <Text style={styles.legendText}>Outdoor</Text>
604
+ </View>
605
+ {cameras.some(c => getCameraById(c.verkadaModel)?.ptz) && (
606
+ <View style={styles.legendItem}>
607
+ <View style={[styles.legendDot, styles.legendDotPtz]} />
608
+ <Text style={styles.legendText}>PTZ</Text>
609
+ </View>
610
+ )}
611
+ </View>
612
+
613
+ {/* Disclaimer */}
614
+ <View style={styles.disclaimer}>
615
+ <Text style={styles.disclaimerText}>
616
+ Report ID: {reportId} | Generated automatically
617
+ </Text>
618
+ </View>
619
+ </View>
620
+ </Page>
621
+ </Document>
622
+ );
623
+ };
624
+
625
+ export default CameraReportPDF;