import React, { useState, useRef, useEffect, useMemo } from 'react'; import { Building, InspectionAct, InspectionItem, InspectionSection, InspectionCommonSection, InspectionMKDElement } from '../../types'; import { buildFloorElements, buildEntranceElements, buildCommonSectionElements } from '../../inspectionElementTemplates'; import { DynamicInspectionSections } from './DynamicInspectionSections'; import { storageService } from '../../services/storageService'; import { backendApi } from '../../services/apiClient'; import { connectionService, type ConnectionStatus } from '../../services/connectionService'; import { Plus, AlertCircle, CheckCircle2, Calendar, ChevronRight, ChevronDown, ChevronUp, Wrench, Save, X, ArrowLeft, MapPin, FileText, Layers, AlertTriangle, Info, Camera, PenTool, Trash2, Eraser, Image as ImageIcon, Paperclip, History, WifiOff, Gauge, DoorClosed, Building2 } from 'lucide-react'; // html2canvas and jsPDF loaded on demand in handleGeneratePDF to keep initial bundle small const InspectionForm: React.FC<{ act: InspectionAct, onSave: (act: InspectionAct) => void, onBack: () => void, building: Building, setBuilding: React.Dispatch> }> = ({ act, onSave, onBack, building, setBuilding }) => { const [formData, setFormData] = useState(act); const [activeSection, setActiveSection] = useState<'meta' | 'dynamic' | 'structure' | 'engineering' | 'media' | 'summary'>( (formData.sections && formData.sections.length > 0) || (formData.commonSections && formData.commonSections.length > 0) ? 'dynamic' : 'meta' ); const [isGeoLoading, setIsGeoLoading] = useState(false); const [isGeneratingPdf, setIsGeneratingPdf] = useState(false); const [connectionStatus, setConnectionStatus] = useState(connectionService.getStatus()); const [isMobile, setIsMobile] = useState(false); // Canvas Logic const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); // Определяем мобильное устройство useEffect(() => { const checkMobile = () => { const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera; const isMobileDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent.toLowerCase()); const isSmallScreen = window.innerWidth < 768; setIsMobile(isMobileDevice || isSmallScreen); }; checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // Подписка на изменения статуса подключения useEffect(() => { const unsubscribe = connectionService.subscribe((status) => { setConnectionStatus(status); }); return () => unsubscribe(); }, []); // Initial Checklist Population if empty useEffect(() => { if (!formData.checklist || formData.checklist.length === 0) { const initialItems: InspectionItem[] = [ // Structural { id: 'str-1', category: 'structural', label: 'Фундамент и отмостка', status: 'not_checked' }, { id: 'str-2', category: 'structural', label: 'Подвал (сухость, свет)', status: 'not_checked' }, { id: 'str-3', category: 'structural', label: 'Фасад и швы', status: 'not_checked' }, { id: 'str-4', category: 'structural', label: 'Кровля и примыкания', status: 'not_checked' }, { id: 'str-5', category: 'structural', label: 'Входные группы', status: 'not_checked' }, // Engineering { id: 'eng-1', category: 'engineering', label: 'ИТП (Отопление)', status: 'not_checked' }, { id: 'eng-2', category: 'engineering', label: 'ХВС / ГВС (Стояки)', status: 'not_checked' }, { id: 'eng-3', category: 'engineering', label: 'Электроснабжение', status: 'not_checked' }, { id: 'eng-4', category: 'engineering', label: 'Лифтовое оборудование', status: 'not_checked' }, ]; setFormData(prev => ({ ...prev, checklist: initialItems })); } }, []); // Load existing signature if available useEffect(() => { if (activeSection === 'summary' && formData.signatureUrl && canvasRef.current) { const ctx = canvasRef.current.getContext('2d'); const img = new Image(); img.onload = () => { ctx?.drawImage(img, 0, 0); }; img.src = formData.signatureUrl; } }, [activeSection]); // Canvas Drawing Handlers const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { setIsDrawing(true); const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; const rect = canvas.getBoundingClientRect(); const x = ('touches' in e) ? e.touches[0].clientX - rect.left : (e as React.MouseEvent).nativeEvent.offsetX; const y = ('touches' in e) ? e.touches[0].clientY - rect.top : (e as React.MouseEvent).nativeEvent.offsetY; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.strokeStyle = '#000'; }; const draw = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Prevent scrolling when drawing on touch devices if ('touches' in e) e.preventDefault(); const rect = canvas.getBoundingClientRect(); const x = ('touches' in e) ? e.touches[0].clientX - rect.left : (e as React.MouseEvent).nativeEvent.offsetX; const y = ('touches' in e) ? e.touches[0].clientY - rect.top : (e as React.MouseEvent).nativeEvent.offsetY; ctx.lineTo(x, y); ctx.stroke(); }; const stopDrawing = () => { if (isDrawing) { setIsDrawing(false); const canvas = canvasRef.current; if (canvas) { const dataUrl = canvas.toDataURL(); setFormData(prev => ({ ...prev, signatureUrl: dataUrl })); } } }; const clearSignature = () => { const canvas = canvasRef.current; if (canvas) { const ctx = canvas.getContext('2d'); ctx?.clearRect(0, 0, canvas.width, canvas.height); setFormData(prev => ({ ...prev, signatureUrl: undefined })); } }; const handleUpdateItem = (id: string, updates: Partial) => { setFormData(prev => { const currentChecklist = prev.checklist || []; return { ...prev, checklist: currentChecklist.map(item => item.id === id ? { ...item, ...updates } : item) }; }); }; const handleAddItemPhoto = (itemId: string) => { const mockPhotoUrl = `https://picsum.photos/400/300?random=${Math.random()}`; setFormData(prev => { const checklist = prev.checklist || []; return { ...prev, checklist: checklist.map(item => { if (item.id !== itemId) return item; const photos = item.photos || []; return { ...item, photos: [...photos, mockPhotoUrl] }; }) }; }); }; const handleRemoveItemPhoto = (itemId: string, photoIdx: number) => { setFormData(prev => { const checklist = prev.checklist || []; return { ...prev, checklist: checklist.map(item => { if (item.id !== itemId) return item; const photos = item.photos || []; return { ...item, photos: photos.filter((_, i) => i !== photoIdx) }; }) }; }); }; const handleGetGeo = () => { setIsGeoLoading(true); setTimeout(() => { setFormData(prev => ({ ...prev, geolocation: { lat: 55.7558, lng: 37.6173, timestamp: new Date().toISOString() } })); setIsGeoLoading(false); }, 1500); }; const handleGeneratePDF = async () => { setIsGeneratingPdf(true); await new Promise(resolve => setTimeout(resolve, 100)); const input = document.getElementById('printable-act'); if (input) { try { const html2canvas = (await import('html2canvas')).default; const { jsPDF } = await import('jspdf'); const canvas = await html2canvas(input, { scale: 2, useCORS: true, logging: false }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF('p', 'mm', 'a4'); const imgWidth = canvas.width; const imgHeight = canvas.height; const finalImgWidth = 190; const finalImgHeight = (imgHeight * finalImgWidth) / imgWidth; pdf.setFontSize(18); pdf.text("Акт осмотра МКД", 105, 20, { align: "center" }); pdf.addImage(imgData, 'PNG', 10, 30, finalImgWidth, finalImgHeight); pdf.save(`Act_${formData.number}.pdf`); } catch (err) { console.error("PDF Gen Error", err); alert("Ошибка генерации PDF"); } } setIsGeneratingPdf(false); }; const issuesCount = (formData.checklist || []).filter(i => i.status === 'repair_needed').length; return (
{/* HIDDEN PRINTABLE AREA FOR PDF GENERATION */}

Акт осмотра общего имущества

№ {formData.number} Дата: {formData.date}

Основание: {formData.basis || 'Плановый осмотр'}

Комиссия: {formData.commissionMembers}

Присутствовал: {formData.ownerRepresentative}

1. Результаты осмотра

{(formData.checklist || []).map(item => ( ))}
Элемент Статус Примечание
{item.label} {item.status === 'ok' ? 'Норма' : item.status === 'repair_needed' ? 'Дефект' : '-'} {item.description} {item.wearPercent ? `(Износ ${item.wearPercent}%)` : ''} {item.photos && item.photos.length > 0 && ` [Фотоотчет: ${item.photos.length} шт.]`}

2. Заключение

Всего дефектов: {issuesCount}

Решение: {formData.workCategory === 'maintenance' ? 'Требуется текущий ремонт' : formData.workCategory === 'capital' ? 'Требуется капитальный ремонт' : 'Удовлетворительно'}

Готовность к зиме: {formData.isReadyForWinter ? 'ДА' : 'НЕТ'}

{formData.signatureUrl && (

Подпись представителя собственников:

Signature
)}
{/* Header */}

Акт осмотра № {formData.number}

{formData.date}

{/* Кнопка подключения - только на мобильных при отсутствии подключения */} {isMobile && connectionStatus === 'disconnected' && ( )}
{/* Navigation Sections */}
{['meta', 'dynamic', 'structure', 'engineering', 'media', 'summary'].map(sec => ( ))}
{/* 1. METADATA */} {activeSection === 'meta' && (
setFormData({...formData, basis: e.target.value})} placeholder="Напр: Приказ №12 от 01.01.24" className="w-full p-3 bg-slate-50 rounded-xl border border-slate-200 outline-none focus:border-primary-500 text-slate-900" />