AssanaliAidarkhan commited on
Commit
2db636c
·
verified ·
1 Parent(s): 00333b7

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +485 -0
index.html ADDED
@@ -0,0 +1,485 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Neurobox AI - Мониторинг СИЗ</title>
7
+ <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
8
+ <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
9
+ <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
19
+ }
20
+ @keyframes pulse-red {
21
+ 0%, 100% { opacity: 1; }
22
+ 50% { opacity: 0.5; }
23
+ }
24
+ .animate-pulse-red {
25
+ animation: pulse-red 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
26
+ }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div id="root"></div>
31
+
32
+ <script type="text/babel">
33
+ const { useState, useEffect, useRef } = React;
34
+
35
+ // Icons
36
+ const Shield = ({ size = 24, className = "" }) => (
37
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
38
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
39
+ </svg>
40
+ );
41
+
42
+ const AlertTriangle = ({ size = 24, className = "" }) => (
43
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
44
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
45
+ <line x1="12" y1="9" x2="12" y2="13"/>
46
+ <line x1="12" y1="17" x2="12.01" y2="17"/>
47
+ </svg>
48
+ );
49
+
50
+ const CheckCircle = ({ size = 24, className = "" }) => (
51
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
52
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
53
+ <polyline points="22 4 12 14.01 9 11.01"/>
54
+ </svg>
55
+ );
56
+
57
+ const Video = ({ size = 24, className = "" }) => (
58
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
59
+ <polygon points="23 7 16 12 23 17 23 7"/>
60
+ <rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
61
+ </svg>
62
+ );
63
+
64
+ const FileText = ({ size = 24, className = "" }) => (
65
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
66
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
67
+ <polyline points="14 2 14 8 20 8"/>
68
+ <line x1="16" y1="13" x2="8" y2="13"/>
69
+ <line x1="16" y1="17" x2="8" y2="17"/>
70
+ </svg>
71
+ );
72
+
73
+ const Archive = ({ size = 24, className = "" }) => (
74
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
75
+ <polyline points="21 8 21 21 3 21 3 8"/>
76
+ <rect x="1" y="3" width="22" height="5"/>
77
+ <line x1="10" y1="12" x2="14" y2="12"/>
78
+ </svg>
79
+ );
80
+
81
+ const Users = ({ size = 24, className = "" }) => (
82
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
83
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
84
+ <circle cx="9" cy="7" r="4"/>
85
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
86
+ <path d="M16 3.13a4 4 0 0 1 0 7.75"/>
87
+ </svg>
88
+ );
89
+
90
+ const Clock = ({ size = 24, className = "" }) => (
91
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
92
+ <circle cx="12" cy="12" r="10"/>
93
+ <polyline points="12 6 12 12 16 14"/>
94
+ </svg>
95
+ );
96
+
97
+ const TrendingDown = ({ size = 24, className = "" }) => (
98
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
99
+ <polyline points="23 18 13.5 8.5 8.5 13.5 1 6"/>
100
+ <polyline points="17 18 23 18 23 12"/>
101
+ </svg>
102
+ );
103
+
104
+ const TrendingUp = ({ size = 24, className = "" }) => (
105
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
106
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/>
107
+ <polyline points="17 6 23 6 23 12"/>
108
+ </svg>
109
+ );
110
+
111
+ const Upload = ({ size = 24, className = "" }) => (
112
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
113
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
114
+ <polyline points="17 8 12 3 7 8"/>
115
+ <line x1="12" y1="3" x2="12" y2="15"/>
116
+ </svg>
117
+ );
118
+
119
+ const PPEMonitoringDashboard = () => {
120
+ const [currentTime, setCurrentTime] = useState(new Date());
121
+ const [activeViolation, setActiveViolation] = useState(null);
122
+ const [violations, setViolations] = useState([]);
123
+ const [uploadedImages, setUploadedImages] = useState({});
124
+
125
+ const canvasRefs = [useRef(null), useRef(null), useRef(null), useRef(null)];
126
+ const imageRefs = [useRef(null), useRef(null), useRef(null), useRef(null)];
127
+ const fileInputRefs = [useRef(null), useRef(null), useRef(null), useRef(null)];
128
+
129
+ // Default image URLs - users can replace these
130
+ const defaultImages = {
131
+ 0: 'https://raw.githubusercontent.com/ultralytics/yolov5/master/data/images/bus.jpg',
132
+ 1: 'https://images.unsplash.com/photo-1581092918484-8313e1f7e8c7?w=400',
133
+ 2: 'https://images.unsplash.com/photo-1581092160562-40aa08e78837?w=400',
134
+ 3: 'https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?w=400'
135
+ };
136
+
137
+ const cameras = [
138
+ { id: 'cam_1', name: 'Cam_1', zone: 'Участок сварки' },
139
+ { id: 'cam_2', name: 'Cam_2', zone: 'Участок прессов' },
140
+ { id: 'cam_3', name: 'Cam_3', zone: 'Участок сборки' },
141
+ { id: 'cam_4', name: 'Cam_4', zone: 'Склад материалов' }
142
+ ];
143
+
144
+ useEffect(() => {
145
+ const timer = setInterval(() => {
146
+ setCurrentTime(new Date());
147
+ }, 1000);
148
+ return () => clearInterval(timer);
149
+ }, []);
150
+
151
+ const handleImageUpload = (cameraIndex, event) => {
152
+ const file = event.target.files[0];
153
+ if (file) {
154
+ const reader = new FileReader();
155
+ reader.onload = (e) => {
156
+ setUploadedImages(prev => ({
157
+ ...prev,
158
+ [cameraIndex]: e.target.result
159
+ }));
160
+ };
161
+ reader.readAsDataURL(file);
162
+ }
163
+ };
164
+
165
+ useEffect(() => {
166
+ const violationInterval = setInterval(() => {
167
+ if (Math.random() < 0.15) {
168
+ const cameraIndex = Math.floor(Math.random() * 4);
169
+ const violationTypes = [
170
+ { type: 'Отсутствие каски', severity: 'critical' },
171
+ { type: 'Отсутствие жилета', severity: 'critical' },
172
+ { type: 'Отсутствие перчаток', severity: 'medium' },
173
+ { type: 'Отсутствие очков', severity: 'medium' }
174
+ ];
175
+ const violation = violationTypes[Math.floor(Math.random() * violationTypes.length)];
176
+
177
+ const newViolation = {
178
+ id: Date.now(),
179
+ camera: cameras[cameraIndex],
180
+ ...violation,
181
+ time: new Date().toLocaleTimeString('ru-RU'),
182
+ cameraIndex
183
+ };
184
+
185
+ setActiveViolation(newViolation);
186
+ setViolations(prev => [newViolation, ...prev.slice(0, 9)]);
187
+
188
+ setTimeout(() => {
189
+ setActiveViolation(null);
190
+ }, 8000);
191
+ }
192
+ }, 5000);
193
+
194
+ return () => clearInterval(violationInterval);
195
+ }, []);
196
+
197
+ useEffect(() => {
198
+ canvasRefs.forEach((canvasRef, index) => {
199
+ const canvas = canvasRef.current;
200
+ const imageRef = imageRefs[index];
201
+ if (!canvas) return;
202
+
203
+ const ctx = canvas.getContext('2d');
204
+ let animationFrame;
205
+
206
+ const animate = () => {
207
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
208
+
209
+ // Draw image if uploaded or use default
210
+ const imageUrl = uploadedImages[index] || defaultImages[index];
211
+ if (imageUrl) {
212
+ const img = new Image();
213
+ img.crossOrigin = "anonymous";
214
+ img.src = imageUrl;
215
+
216
+ img.onload = () => {
217
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
218
+
219
+ // Draw detection boxes and overlays
220
+ const hasViolation = activeViolation?.cameraIndex === index;
221
+
222
+ if (hasViolation) {
223
+ ctx.strokeStyle = '#ef4444';
224
+ ctx.lineWidth = 6;
225
+ ctx.setLineDash([15, 10]);
226
+ ctx.strokeRect(100, 50, 200, 250);
227
+ ctx.setLineDash([]);
228
+
229
+ ctx.fillStyle = '#ef4444';
230
+ ctx.beginPath();
231
+ ctx.arc(280, 70, 25, 0, Math.PI * 2);
232
+ ctx.fill();
233
+ ctx.fillStyle = '#ffffff';
234
+ ctx.font = 'bold 30px Arial';
235
+ ctx.fillText('!', 273, 85);
236
+
237
+ ctx.fillStyle = '#ef4444';
238
+ ctx.fillRect(110, 290, 180, 30);
239
+ ctx.fillStyle = '#ffffff';
240
+ ctx.font = 'bold 14px Arial';
241
+ ctx.fillText(activeViolation.type, 120, 310);
242
+ } else {
243
+ ctx.strokeStyle = '#22c55e';
244
+ ctx.lineWidth = 4;
245
+ ctx.strokeRect(100, 50, 200, 250);
246
+
247
+ ctx.strokeStyle = '#22c55e';
248
+ ctx.lineWidth = 3;
249
+ ctx.beginPath();
250
+ ctx.moveTo(270, 70);
251
+ ctx.lineTo(280, 80);
252
+ ctx.lineTo(295, 65);
253
+ ctx.stroke();
254
+ }
255
+
256
+ // Camera label
257
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
258
+ ctx.fillRect(10, 10, 85, 28);
259
+ ctx.fillStyle = '#ffffff';
260
+ ctx.font = 'bold 14px Arial';
261
+ ctx.fillText(cameras[index].name, 20, 30);
262
+
263
+ // Timestamp
264
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
265
+ ctx.fillRect(canvas.width - 100, canvas.height - 30, 90, 20);
266
+ ctx.fillStyle = '#ffffff';
267
+ ctx.font = '10px monospace';
268
+ ctx.fillText(new Date().toLocaleTimeString('ru-RU'), canvas.width - 95, canvas.height - 15);
269
+
270
+ // Status indicator
271
+ ctx.fillStyle = hasViolation ? '#ef4444' : '#22c55e';
272
+ ctx.beginPath();
273
+ ctx.arc(canvas.width - 15, 20, 10, 0, Math.PI * 2);
274
+ ctx.fill();
275
+ };
276
+ } else {
277
+ // Fallback background
278
+ ctx.fillStyle = '#1e293b';
279
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
280
+
281
+ ctx.fillStyle = '#64748b';
282
+ ctx.font = '16px Arial';
283
+ ctx.textAlign = 'center';
284
+ ctx.fillText('Загрузите фото', canvas.width / 2, canvas.height / 2);
285
+ }
286
+
287
+ animationFrame = requestAnimationFrame(animate);
288
+ };
289
+
290
+ animate();
291
+ return () => cancelAnimationFrame(animationFrame);
292
+ });
293
+ }, [activeViolation, uploadedImages]);
294
+
295
+ return (
296
+ <div className="min-h-screen bg-gradient-to-br from-orange-50 via-white to-red-50 p-6">
297
+ <div className="max-w-7xl mx-auto">
298
+ <div className="bg-gradient-to-r from-orange-500 to-red-600 rounded-xl shadow-2xl p-6 mb-6">
299
+ <div className="flex justify-between items-center">
300
+ <div className="flex items-center gap-4">
301
+ <div className="bg-white p-2 rounded-lg">
302
+ <Shield size={32} className="text-red-600" />
303
+ </div>
304
+ <div>
305
+ <h1 className="text-3xl font-bold text-white">Мониторинг СИЗ</h1>
306
+ <p className="text-orange-100 text-sm">Контроль использования средств индивидуальной защиты</p>
307
+ </div>
308
+ </div>
309
+ <div className="bg-white px-4 py-2 rounded-lg shadow-md">
310
+ <Clock size={20} className="inline text-red-600 mr-2" />
311
+ <span className="font-mono text-lg text-gray-800 font-semibold">
312
+ {currentTime.toLocaleTimeString('ru-RU')}
313
+ </span>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ <div className="grid grid-cols-4 gap-6 mb-6">
319
+ {cameras.map((camera, index) => (
320
+ <div
321
+ key={camera.id}
322
+ className={`bg-white rounded-xl shadow-xl overflow-hidden border-2 ${
323
+ activeViolation?.cameraIndex === index
324
+ ? 'border-red-500 animate-pulse-red'
325
+ : 'border-green-500'
326
+ }`}
327
+ >
328
+ <div className={`p-3 ${
329
+ activeViolation?.cameraIndex === index
330
+ ? 'bg-red-500'
331
+ : 'bg-green-500'
332
+ }`}>
333
+ <div className="flex items-center justify-between">
334
+ <div className="flex items-center gap-2">
335
+ <Video size={18} className="text-white" />
336
+ <span className="font-bold text-white text-sm">{camera.name}</span>
337
+ </div>
338
+ <span className={`w-2 h-2 rounded-full ${
339
+ activeViolation?.cameraIndex === index ? 'bg-white animate-pulse' : 'bg-white'
340
+ }`}></span>
341
+ </div>
342
+ <p className="text-xs text-white mt-1">{camera.zone}</p>
343
+ </div>
344
+ <div className="relative">
345
+ <canvas
346
+ ref={canvasRefs[index]}
347
+ width={400}
348
+ height={300}
349
+ className="w-full"
350
+ />
351
+ <button
352
+ onClick={() => fileInputRefs[index].current?.click()}
353
+ className="absolute bottom-2 right-2 bg-blue-600 hover:bg-blue-700 text-white p-2 rounded-lg shadow-lg transition-colors"
354
+ title="Загрузить фото"
355
+ >
356
+ <Upload size={18} />
357
+ </button>
358
+ <input
359
+ ref={fileInputRefs[index]}
360
+ type="file"
361
+ accept="image/*"
362
+ onChange={(e) => handleImageUpload(index, e)}
363
+ className="hidden"
364
+ />
365
+ </div>
366
+ </div>
367
+ ))}
368
+ </div>
369
+
370
+ <div className="grid grid-cols-3 gap-6">
371
+ <div className="col-span-2 bg-white rounded-xl shadow-xl p-6">
372
+ <h2 className="text-xl font-bold text-gray-800 mb-4 flex items-center gap-2">
373
+ <AlertTriangle className="text-red-600" />
374
+ Активные нарушения
375
+ </h2>
376
+ {activeViolation ? (
377
+ <div className="bg-red-50 border-2 border-red-500 rounded-lg p-4">
378
+ <div className="flex items-start gap-4">
379
+ <div className="bg-red-500 p-2 rounded-lg">
380
+ <AlertTriangle className="text-white" size={24} />
381
+ </div>
382
+ <div className="flex-1">
383
+ <div className="flex justify-between items-start mb-2">
384
+ <h3 className="font-bold text-red-900 text-lg">{activeViolation.type}</h3>
385
+ <span className={`px-3 py-1 rounded-full text-xs font-bold ${
386
+ activeViolation.severity === 'critical'
387
+ ? 'bg-red-600 text-white'
388
+ : 'bg-yellow-500 text-white'
389
+ }`}>
390
+ {activeViolation.severity === 'critical' ? 'КРИТИЧЕСКАЯ' : 'СРЕДНЯЯ'}
391
+ </span>
392
+ </div>
393
+ <div className="space-y-1 text-sm text-gray-700">
394
+ <p><strong>Зона:</strong> {activeViolation.camera.zone}</p>
395
+ <p><strong>Камера:</strong> {activeViolation.camera.name}</p>
396
+ <p><strong>Время:</strong> {activeViolation.time}</p>
397
+ </div>
398
+ <div className="flex gap-2 mt-3">
399
+ <button className="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2">
400
+ <FileText size={16} />
401
+ Создать отчет
402
+ </button>
403
+ <button className="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center gap-2">
404
+ <Archive size={16} />
405
+ В архив
406
+ </button>
407
+ </div>
408
+ </div>
409
+ </div>
410
+ </div>
411
+ ) : (
412
+ <div className="bg-green-50 border-2 border-green-500 rounded-lg p-8 text-center">
413
+ <CheckCircle className="mx-auto text-green-600 mb-3" size={48} />
414
+ <p className="text-green-800 font-semibold text-lg">Нарушений не обнаружено</p>
415
+ <p className="text-green-600 text-sm mt-1">Все работники используют СИЗ</p>
416
+ </div>
417
+ )}
418
+
419
+ <h3 className="text-lg font-bold text-gray-800 mt-6 mb-3">История нарушений</h3>
420
+ <div className="space-y-2 max-h-80 overflow-y-auto">
421
+ {violations.length > 0 ? violations.map((violation) => (
422
+ <div key={violation.id} className="bg-gray-50 border border-gray-200 rounded-lg p-3 flex items-center gap-3">
423
+ <div className={`w-2 h-2 rounded-full ${
424
+ violation.severity === 'critical' ? 'bg-red-500' : 'bg-yellow-500'
425
+ }`}></div>
426
+ <div className="flex-1">
427
+ <p className="font-semibold text-sm text-gray-800">{violation.type}</p>
428
+ <p className="text-xs text-gray-600">{violation.camera.zone} • {violation.camera.name} • {violation.time}</p>
429
+ </div>
430
+ </div>
431
+ )) : (
432
+ <p className="text-center text-gray-500 py-8">Нет записей о нарушениях</p>
433
+ )}
434
+ </div>
435
+ </div>
436
+
437
+ <div className="space-y-6">
438
+ <div className="bg-white rounded-xl shadow-xl p-6">
439
+ <h2 className="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
440
+ <Users className="text-blue-600" />
441
+ Статистика за смену
442
+ </h2>
443
+ <div className="space-y-4">
444
+ <div className="bg-red-50 rounded-lg p-4 border border-red-200">
445
+ <p className="text-sm text-red-700 mb-1">Нарушений обнаружено</p>
446
+ <p className="text-3xl font-bold text-red-600">{violations.length}</p>
447
+ </div>
448
+ <div className="bg-green-50 rounded-lg p-4 border border-green-200">
449
+ <div className="flex items-center gap-2 mb-1">
450
+ <TrendingDown size={16} className="text-green-600" />
451
+ <p className="text-sm text-green-700">Снижение травматизма</p>
452
+ </div>
453
+ <p className="text-3xl font-bold text-green-600">25%</p>
454
+ </div>
455
+ <div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
456
+ <div className="flex items-center gap-2 mb-1">
457
+ <TrendingUp size={16} className="text-blue-600" />
458
+ <p className="text-sm text-blue-700">Повышение дисциплины</p>
459
+ </div>
460
+ <p className="text-3xl font-bold text-blue-600">80%</p>
461
+ </div>
462
+ </div>
463
+ </div>
464
+
465
+ <div className="bg-gradient-to-br from-orange-600 to-red-700 rounded-xl shadow-xl p-6 text-white">
466
+ <h2 className="text-lg font-bold mb-4">Важность СИЗ</h2>
467
+ <div className="space-y-3 text-sm">
468
+ <p>✓ Защита от травм и несчастных случаев</p>
469
+ <p>✓ Соблюдение требований охраны труда</p>
470
+ <p>✓ Снижение финансовых рисков</p>
471
+ <p>✓ Формирование доказательной базы</p>
472
+ </div>
473
+ </div>
474
+ </div>
475
+ </div>
476
+ </div>
477
+ </div>
478
+ );
479
+ };
480
+
481
+ const root = ReactDOM.createRoot(document.getElementById('root'));
482
+ root.render(<PPEMonitoringDashboard />);
483
+ </script>
484
+ </body>
485
+ </html>