import React, { useState, useMemo, useEffect } from 'react'; import { Building, PassportMeter, PassportServiceContract } from '../../types'; import { FileText, Layers, Zap, Trees, Briefcase, ArrowUpFromLine, Gauge, Droplets, Flame, X, BarChart3, History, ChevronRight, ShieldCheck, Wifi, Video, Trash2, Plus, Globe, HardHat, Upload, File, Calendar, Edit2, Save, Clock, TrendingUp } from 'lucide-react'; import { EditableField } from './EditableField'; import { AddCustomFieldModal } from './AddCustomFieldModal'; import { storageService } from '../../services/storageService'; // Mock Data Generator for ODPU History const generateODPUHistory = (resource: string) => { const data = []; const today = new Date(); let currentVal = resource === 'Electricity' ? 450000 : resource === 'Water' ? 12000 : 8500; for (let i = 0; i < 12; i++) { const date = new Date(today.getFullYear(), today.getMonth() - i, 1); let consumption = 0; if (resource === 'Heat') { const isWinter = date.getMonth() < 3 || date.getMonth() > 9; consumption = isWinter ? 150 + Math.random() * 50 : 0; } else if (resource === 'Water') { consumption = 800 + Math.random() * 100; } else { consumption = 12000 + Math.random() * 2000; } data.push({ date: date.toISOString().split('T')[0], value: Math.floor(currentVal), consumption: Math.floor(consumption) }); currentVal -= consumption; } return data.reverse(); }; // Модальное окно для создания нового ПУ const AddMeterModal: React.FC<{ isOpen: boolean; onClose: () => void; onAdd: (meter: PassportMeter) => void; }> = ({ isOpen, onClose, onAdd }) => { const [resource, setResource] = useState<'Heat' | 'Water' | 'Electricity' | 'Gas' | 'Other'>('Heat'); const [name, setName] = useState(''); const [number, setNumber] = useState(''); const [model, setModel] = useState(''); const [manufacturer, setManufacturer] = useState(''); const [unit, setUnit] = useState(''); const [installDate, setInstallDate] = useState(''); const [lastVerification, setLastVerification] = useState(''); const [nextVerification, setNextVerification] = useState(''); const [currentReading, setCurrentReading] = useState(''); const defaultUnits: Record = { 'Heat': 'Гкал', 'Water': 'м³', 'Electricity': 'кВт⋅ч', 'Gas': 'м³', 'Other': '' }; const defaultNames: Record = { 'Heat': 'Отопление', 'Water': 'Водоснабжение', 'Electricity': 'Электроснабжение', 'Gas': 'Газоснабжение', 'Other': '' }; const handleResourceChange = (newResource: typeof resource) => { setResource(newResource); if (!name || name === defaultNames[resource]) { setName(defaultNames[newResource] || ''); } if (!unit || unit === defaultUnits[resource]) { setUnit(defaultUnits[newResource] || ''); } }; const handleSubmit = () => { if (!name.trim() || !number.trim()) { alert('Заполните название и номер прибора'); return; } const newMeter: PassportMeter = { id: `meter-${Date.now()}`, resource, hasMeter: true, name: name.trim(), number: number.trim(), model: model.trim() || undefined, manufacturer: manufacturer.trim() || undefined, unit: unit.trim() || defaultUnits[resource] || undefined, installDate: installDate || undefined, lastVerification: lastVerification || undefined, nextVerification: nextVerification || undefined, currentReading: currentReading ? parseFloat(currentReading) : undefined, readings: currentReading ? [{ date: new Date().toISOString().split('T')[0], value: parseFloat(currentReading), consumption: 0, source: 'manual' }] : [], documents: [] }; onAdd(newMeter); // Сброс формы setResource('Heat'); setName(''); setNumber(''); setModel(''); setManufacturer(''); setUnit(''); setInstallDate(''); setLastVerification(''); setNextVerification(''); setCurrentReading(''); onClose(); }; if (!isOpen) return null; return (
e.stopPropagation()}>

Добавить прибор учета

setName(e.target.value)} placeholder={defaultNames[resource] || 'Введите название'} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setNumber(e.target.value)} placeholder="Введите номер" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setModel(e.target.value)} placeholder="Модель" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setManufacturer(e.target.value)} placeholder="Производитель" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setUnit(e.target.value)} placeholder={defaultUnits[resource] || 'Единица измерения'} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setInstallDate(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setCurrentReading(e.target.value)} placeholder="0" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setLastVerification(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setNextVerification(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
); }; // Модальное окно карточки ПУ const MeterCardModal: React.FC<{ meter: PassportMeter; meterIndex: number; isEditing: boolean; onClose: () => void; onUpdate: (index: number, field: string, value: any) => void; onAddReading: (index: number, reading: { date: string; value: number; consumption: number }) => void; }> = ({ meter, meterIndex, isEditing, onClose, onUpdate, onAddReading }) => { const [isEditingCard, setIsEditingCard] = useState(false); const [newReadingDate, setNewReadingDate] = useState(''); const [newReadingValue, setNewReadingValue] = useState(''); const history = useMemo(() => { if (meter.readings && meter.readings.length > 0) { // Сортируем по дате (от новых к старым) и рассчитываем потребление, если его нет const sorted = [...meter.readings].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Если потребление не указано, рассчитываем его из разницы показаний return sorted.map((reading, index) => { if (reading.consumption !== undefined && reading.consumption > 0) { return reading; } // Ищем предыдущее показание для расчета потребления const previousReading = index < sorted.length - 1 ? sorted[index + 1] : null; if (previousReading && previousReading.value !== undefined) { const consumption = Math.max(0, reading.value - previousReading.value); return { ...reading, consumption }; } return { ...reading, consumption: 0 }; }); } return []; }, [meter.readings]); // Статистика по показаниям (только из реальных данных) const stats = useMemo(() => { if (!history || history.length === 0) { return { total: 0, average: 0, max: 0, min: 0, count: 0, period: { start: null, end: null } }; } // Берем только записи с потреблением > 0 const readingsWithConsumption = history.filter(r => r.consumption !== undefined && r.consumption > 0); if (readingsWithConsumption.length === 0) { return { total: 0, average: 0, max: 0, min: 0, count: history.length, period: history.length > 0 ? { start: new Date(Math.min(...history.map(r => new Date(r.date).getTime()))), end: new Date(Math.max(...history.map(r => new Date(r.date).getTime()))) } : { start: null, end: null } }; } const consumptions = readingsWithConsumption.map(r => r.consumption || 0); const dates = readingsWithConsumption.map(r => new Date(r.date)); return { total: consumptions.reduce((sum, val) => sum + val, 0), average: consumptions.reduce((sum, val) => sum + val, 0) / consumptions.length, max: Math.max(...consumptions), min: Math.min(...consumptions), count: history.length, // Общее количество записей period: { start: new Date(Math.min(...dates.map(d => d.getTime()))), end: new Date(Math.max(...dates.map(d => d.getTime()))) } }; }, [history]); const maxVal = Math.max(...history.map(r => r.consumption || 0), 1); let colorClass = 'bg-slate-500'; let textClass = 'text-slate-600'; let label: string = meter.name || meter.resource; let unit = meter.unit || 'ед.'; let Icon = Gauge; switch(meter.resource) { case 'Heat': Icon = Flame; colorClass = 'bg-red-400'; textClass = 'text-red-600'; label = meter.name || 'Отопление'; unit = meter.unit || 'Гкал'; break; case 'Water': Icon = Droplets; colorClass = 'bg-blue-400'; textClass = 'text-blue-600'; label = meter.name || 'Водоснабжение'; unit = meter.unit || 'м³'; break; case 'Electricity': Icon = Zap; colorClass = 'bg-amber-400'; textClass = 'text-amber-600'; label = meter.name || 'Электроснабжение'; unit = meter.unit || 'кВт⋅ч'; break; case 'Gas': Icon = Gauge; colorClass = 'bg-slate-500'; textClass = 'text-slate-600'; label = meter.name || 'Газоснабжение'; unit = meter.unit || 'м³'; break; case 'Other': Icon = Gauge; colorClass = 'bg-slate-500'; textClass = 'text-slate-600'; label = meter.name || 'Другой'; break; } const handleAddReading = () => { if (!newReadingDate || !newReadingValue) return; const value = parseFloat(newReadingValue); if (isNaN(value)) return; const previousReading = history.length > 0 ? history[0].value : 0; const consumption = Math.max(0, value - previousReading); onAddReading(meterIndex, { date: newReadingDate, value: value, consumption: consumption, source: 'manual' }); setNewReadingDate(''); setNewReadingValue(''); }; return (
e.stopPropagation()}>

{label}

№ {meter.number || 'Не указан'} {meter.model && `• ${meter.model}`}

{isEditing && ( )}
{/* Основная информация */}
{isEditingCard ? ( ) : (

{label}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'name', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.name || label}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'number', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.number || 'Не указан'}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'model', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.model || 'Не указана'}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'manufacturer', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.manufacturer || 'Не указан'}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'installDate', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.installDate || 'Не указана'}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'lastVerification', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.lastVerification || 'Не указана'}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'nextVerification', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.nextVerification || 'Не указана'}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'unit', e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{unit}

)}
{isEditingCard ? ( onUpdate(meterIndex, 'currentReading', parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> ) : (

{meter.currentReading?.toLocaleString('ru-RU') || 'Не указано'}

)}
{/* Статистика */} {stats.count > 0 && (

Статистика показаний

Общее потребление

{stats.total.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}

Среднее потребление

{stats.average.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}

Максимальное

{stats.max.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}

Минимальное

{stats.min.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}

Период

{stats.period.start && stats.period.end ? `${stats.period.start.toLocaleDateString('ru-RU')} - ${stats.period.end.toLocaleDateString('ru-RU')}` : 'Не указан'}

Всего записей: {stats.count}

)} {/* История показаний */}

История показаний

{isEditingCard && (
setNewReadingDate(e.target.value)} className="px-2 py-1 border border-slate-300 rounded text-xs" placeholder="Дата" /> setNewReadingValue(e.target.value)} className="px-2 py-1 border border-slate-300 rounded text-xs w-24" placeholder="Показание" />
)}
{history.length > 0 ? (
{history.map((reading, idx) => (
{new Date(reading.date).toLocaleDateString('ru-RU')}
{reading.value?.toLocaleString('ru-RU', { maximumFractionDigits: 2 }) || 0} {unit} {reading.consumption !== undefined && reading.consumption > 0 && ( {reading.consumption.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit} )}
))}
) : (

Нет данных о показаниях. Добавьте первое показание.

)}
{/* График потребления (только реальные данные) */} {history.length > 0 && (() => { const readingsWithConsumption = history.filter(r => r.consumption !== undefined && r.consumption > 0); const maxConsumption = Math.max(...readingsWithConsumption.map(r => r.consumption || 0), 1); return readingsWithConsumption.length > 0 ? (

Потребление за период ({unit})

{readingsWithConsumption.slice(0, 12).reverse().map((r, i) => (
))}
) : null; })()} {/* Документы */} {isEditingCard && (
{ if (!e.target.files) return; const fileList = e.target.files; const newFiles = Array.from(fileList).map((file: File) => URL.createObjectURL(file as Blob)); const updatedDocuments = [...(meter.documents || []), ...newFiles]; onUpdate(meterIndex, 'documents', updatedDocuments); }} className="text-xs" /> {meter.documents && meter.documents.length > 0 && (
{meter.documents.map((doc, docIdx) => (
Документ {docIdx + 1}
))}
)}
)}
); }; // Вынесенный компонент поля паспорта — стабильная идентичность предотвращает потерю фокуса при вводе const PassportField: React.FC<{ label: string; value: any; section: keyof Building['passport']; fieldKey: string; type?: string; files?: string[]; onFilesChange?: (files: string[]) => void; isCustomField?: boolean; customFieldKey?: string; isEditing: boolean; onValueChange: (section: keyof Building['passport'], fieldKey: string, value: any, opts?: { isCustomField: boolean; customFieldKey?: string }) => void; }> = ({ label, value, section, fieldKey, type = 'text', files, onFilesChange, isCustomField = false, customFieldKey, isEditing, onValueChange }) => { const handleFileUpload = (e: React.ChangeEvent) => { if (!e.target.files || !onFilesChange) return; const fileList = e.target.files; const newFiles = Array.from(fileList).map((file: File) => { return URL.createObjectURL(file as Blob); }); onFilesChange([...(files || []), ...newFiles]); }; const handleValueChange = (v: string) => { setTimeout(() => { try { let processedValue: any = v; if (type === 'number') { if (v === '' || v === null || v === undefined) { processedValue = null; } else { const numValue = Number(v); processedValue = isNaN(numValue) ? null : numValue; } } else if (type === 'checkbox') { processedValue = v === 'true'; } onValueChange(section, fieldKey, processedValue, { isCustomField, customFieldKey }); } catch (error) { console.error('Error in handleValueChange:', error, { section, fieldKey, value: v, type }); } }, 0); }; return (
{isEditing && onFilesChange && (
{files && files.length > 0 && (
{files.map((file, idx) => (
{file}
))}
)}
)} {!isEditing && files && files.length > 0 && (
{files.map((file, idx) => ( Файл {idx + 1} ))}
)}
); }; export const PassportView: React.FC<{ building: Building; isEditing: boolean; updatePassport: (section: keyof Building['passport'], field: string, value: any) => void; updatePassportArray: (section: 'meters' | 'lifts', index: number, field: string, value: any) => void; }> = ({ building, isEditing, updatePassport, updatePassportArray }) => { const { general, construction, engineering, odpu = { customFields: {} }, land, management = { contractDate: '', contractNumber: '', tariffMaintenance: null, reserveFund: null, serviceContracts: [], customFields: {} }, meters, lifts } = building.passport; const [selectedMeter, setSelectedMeter] = useState<{ meter: PassportMeter; index: number } | null>(null); const [showAddFieldModal, setShowAddFieldModal] = useState(false); const [showAddMeterModal, setShowAddMeterModal] = useState(false); const [currentSection, setCurrentSection] = useState<'general' | 'construction' | 'engineering' | 'odpu' | 'land' | 'management' | null>(null); const serviceContracts = management.serviceContracts || []; // Вычисляем данные из лицевых счетов const accountsStats = useMemo(() => { const accounts = building.accounts || []; const residential = accounts.filter(a => a.type === 'apartment').length; const nonResidential = accounts.filter(a => a.type === 'storage' || a.type === 'parking').length; const offices = accounts.filter(a => a.type === 'office').length; const apartments = accounts.filter(a => a.type === 'apartment').length; const nonLivingCommonArea = accounts .filter(a => a.type === 'storage' || a.type === 'parking') .reduce((sum, a) => sum + (a.areaNonLiving || 0), 0); return { residentialAccountsCount: residential, nonResidentialAccountsCount: nonResidential, officesAccountsCount: offices, apartmentsCount: apartments, nonLivingCommonArea }; }, [building.accounts]); const handleAddContract = () => { const newContract: PassportServiceContract = { id: `sc-${Date.now()}`, serviceType: 'Новая услуга', providerName: 'Наименование организации', contractNumber: '№ договора', contractDate: new Date().toISOString().split('T')[0] }; updatePassport('management', 'serviceContracts', [...serviceContracts, newContract]); }; const handleUpdateContract = (id: string, field: keyof PassportServiceContract, value: any) => { const updated = serviceContracts.map(c => c.id === id ? { ...c, [field]: value } : c); updatePassport('management', 'serviceContracts', updated); }; const handleRemoveContract = (id: string) => { updatePassport('management', 'serviceContracts', serviceContracts.filter(c => c.id !== id)); }; const Section: React.FC<{ title: string; children: React.ReactNode; icon?: React.ElementType; noGrid?: boolean }> = ({ title, children, icon: Icon, noGrid }) => (

{Icon && } {title} {isEditing && (
)}

{children}
); const getServiceIcon = (type: string) => { const lower = type.toLowerCase(); if (lower.includes('вод')) return Droplets; if (lower.includes('газ')) return Flame; if (lower.includes('свет') || lower.includes('энерго')) return Zap; if (lower.includes('интернет') || lower.includes('связь')) return Wifi; if (lower.includes('видео') || lower.includes('наблюд')) return Video; if (lower.includes('клин') || lower.includes('уборк')) return HardHat; if (lower.includes('лифт')) return ArrowUpFromLine; return FileText; }; const handleFieldValueChange = (section: keyof Building['passport'], fieldKey: string, value: any, opts?: { isCustomField: boolean; customFieldKey?: string }) => { if (opts?.isCustomField && opts?.customFieldKey && (section === 'general' || section === 'construction' || section === 'engineering' || section === 'odpu' || section === 'land' || section === 'management')) { updateCustomField(section, opts.customFieldKey, value); } else { updatePassport(section, fieldKey, value); } }; // Функция для работы с кастомными полями const updateCustomField = (section: 'general' | 'construction' | 'engineering' | 'odpu' | 'land' | 'management', fieldKey: string, value: any, files?: string[]) => { // Проверяем, является ли это поле глобальным const isGlobal = globalFields[section] && globalFields[section][fieldKey]; if (isGlobal) { // Обновляем глобальное поле (это обновит все дома) (storageService.updateGlobalPassportField as any)(section, fieldKey, value, files); // Обновляем текущий дом const sectionData = building.passport[section] as any; const customFields = { ...(sectionData.customFields || {}) }; customFields[fieldKey] = { value, type: globalFields[section][fieldKey].type, files: files !== undefined ? files : (customFields[fieldKey]?.files || []) }; updatePassport(section, 'customFields', customFields); } else { // Обновляем только для текущего дома const sectionData = building.passport[section] as any; const customFields = { ...(sectionData.customFields || {}) }; customFields[fieldKey] = { value, type: typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'checkbox' : 'text', files: files !== undefined ? files : (customFields[fieldKey]?.files || []) }; updatePassport(section, 'customFields', customFields); } }; const addCustomField = (section: 'general' | 'construction' | 'engineering' | 'odpu' | 'land' | 'management') => { setCurrentSection(section); setShowAddFieldModal(true); }; const handleConfirmAddField = (fieldName: string, fieldType: 'text' | 'number' | 'checkbox', forAllBuildings: boolean) => { if (!currentSection) return; if (forAllBuildings) { // Сохраняем как глобальное поле для всех домов storageService.saveGlobalPassportField(currentSection, fieldName, fieldType); // Обновляем текущий дом, чтобы поле сразу отобразилось const globalFields = storageService.getGlobalPassportFields(); if (globalFields[currentSection] && globalFields[currentSection][fieldName]) { const sectionData = building.passport[currentSection] as any; const customFields = { ...(sectionData.customFields || {}) }; customFields[fieldName] = globalFields[currentSection][fieldName]; updatePassport(currentSection, 'customFields', customFields); } } else { // Создаем поле только для текущего дома const sectionData = building.passport[currentSection] as any; const customFields = { ...(sectionData.customFields || {}) }; const defaultValue = fieldType === 'number' ? 0 : fieldType === 'checkbox' ? false : ''; customFields[fieldName] = { value: defaultValue, type: fieldType, files: [] }; updatePassport(currentSection, 'customFields', customFields); } }; // Загружаем глобальные поля при монтировании компонента const globalFields = useMemo(() => { return storageService.getGlobalPassportFields(); }, []); // Применяем глобальные поля к текущему дому при загрузке useEffect(() => { if (!isEditing) return; // Применяем только в режиме редактирования (['general', 'construction', 'engineering', 'odpu', 'land', 'management'] as const).forEach(section => { if (globalFields[section]) { const sectionData = building.passport[section] as any; const customFields = { ...(sectionData.customFields || {}) }; // Добавляем глобальные поля, если их еще нет Object.keys(globalFields[section]).forEach(fieldName => { if (!customFields[fieldName]) { customFields[fieldName] = { ...globalFields[section][fieldName] }; const updatedSection = { ...sectionData, customFields }; updatePassport(section, 'customFields', customFields); } }); } }); }, [isEditing, globalFields]); return (
{showAddFieldModal && ( { setShowAddFieldModal(false); setCurrentSection(null); }} onConfirm={handleConfirmAddField} section={currentSection || undefined} /> )}
{/* Новые поля */} {/* Вычисляемые поля из лицевых счетов */}
Жилых лицевых счетов:{accountsStats.residentialAccountsCount}
Нежилых лицевых счетов:{accountsStats.nonResidentialAccountsCount}
Офисов (из счетов):{accountsStats.officesAccountsCount}
Квартир (из счетов):{accountsStats.apartmentsCount}
Площадь нежилых в ОДИ (м²):{accountsStats.nonLivingCommonArea.toFixed(2)}
{/* Глобальные поля (для всех домов) */} {globalFields.general && Object.keys(globalFields.general).map(key => { const globalField = globalFields.general[key]; const localField = general.customFields?.[key] || globalField; const isGlobal = true; return (
updateCustomField('general', key, localField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {/* Локальные кастомные поля (только для этого дома) */} {general.customFields && Object.keys(general.customFields) .filter(key => !(globalFields.general && globalFields.general[key])) .map(key => { const customField = general.customFields![key]; return (
updateCustomField('general', key, customField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {isEditing && ( )}
{/* Новые поля */} {/* Глобальные поля (для всех домов) */} {globalFields.construction && Object.keys(globalFields.construction).map(key => { const globalField = globalFields.construction[key]; const localField = construction.customFields?.[key] || globalField; return (
updateCustomField('construction', key, localField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {/* Локальные кастомные поля (только для этого дома) */} {construction.customFields && Object.keys(construction.customFields) .filter(key => !(globalFields.construction && globalFields.construction[key])) .map(key => { const customField = construction.customFields![key]; return (
updateCustomField('construction', key, customField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {isEditing && ( )}
{/* Глобальные поля (для всех домов) */} {globalFields.engineering && Object.keys(globalFields.engineering).map(key => { const globalField = globalFields.engineering[key]; const localField = engineering.customFields?.[key] || globalField; return (
updateCustomField('engineering', key, localField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {/* Локальные кастомные поля (только для этого дома) */} {engineering.customFields && Object.keys(engineering.customFields) .filter(key => !(globalFields.engineering && globalFields.engineering[key])) .map(key => { const customField = engineering.customFields![key]; return (
updateCustomField('engineering', key, customField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {isEditing && ( )}
{/* Список всех ПУ */} {meters.filter(m => m.hasMeter).map((meter, idx) => { let Icon = Gauge; let colorClass = 'text-slate-500 bg-slate-50'; let label: string = meter.name || meter.resource; let unit = meter.unit || 'ед.'; switch(meter.resource) { case 'Heat': Icon = Flame; colorClass = 'text-red-500 bg-red-50'; label = meter.name || 'Отопление'; unit = meter.unit || 'Гкал'; break; case 'Water': Icon = Droplets; colorClass = 'text-blue-500 bg-blue-50'; label = meter.name || 'Водоснабжение'; unit = meter.unit || 'м³'; break; case 'Electricity': Icon = Zap; colorClass = 'text-amber-500 bg-amber-50'; label = meter.name || 'Электроснабжение'; unit = meter.unit || 'кВт⋅ч'; break; case 'Gas': Icon = Gauge; colorClass = 'text-slate-500 bg-slate-50'; label = meter.name || 'Газоснабжение'; unit = meter.unit || 'м³'; break; case 'Other': Icon = Gauge; colorClass = 'text-slate-500 bg-slate-50'; label = meter.name || 'Другой'; break; } const actualIdx = meters.findIndex(m => m === meter); return (
setSelectedMeter({ meter, index: actualIdx })} className="p-4 rounded-xl border border-slate-200 bg-white shadow-sm cursor-pointer hover:shadow-md transition-all" >

{label}

{meter.number && (

№ {meter.number}

)}
{isEditing && ( )}
{meter.currentReading !== undefined && (
Текущее показание:

{meter.currentReading.toLocaleString('ru-RU')} {unit}

)} {meter.nextVerification && (
Поверка до:

{new Date(meter.nextVerification).toLocaleDateString('ru-RU')}

)}
); })} {meters.filter(m => m.hasMeter).length === 0 && (

Нет добавленных приборов учета

)} {isEditing && ( )}
{/* Модальное окно создания ПУ */} setShowAddMeterModal(false)} onAdd={(newMeter) => { const updatedMeters = [...meters, newMeter]; updatePassport('meters' as any, 'meters' as any, updatedMeters); // Открываем карточку нового ПУ setTimeout(() => { setSelectedMeter({ meter: newMeter, index: meters.length }); }, 100); }} /> {/* Модальное окно карточки ПУ */} {selectedMeter && ( setSelectedMeter(null)} onUpdate={(index, field, value) => { updatePassportArray('meters', index, field, value); }} onAddReading={(index, reading) => { const meter = meters[index]; const updatedReadings = [...(meter.readings || []), reading]; updatePassportArray('meters', index, 'readings', updatedReadings); // Обновляем текущее показание updatePassportArray('meters', index, 'currentReading', reading.value); }} /> )}
{lifts.map((lift, idx) => (

Лифт №{idx + 1}

{isEditing && ( )}
updatePassportArray('lifts', idx, 'type', v)} isEditing={isEditing} className="text-right w-full"/>
updatePassportArray('lifts', idx, 'installYear', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
updatePassportArray('lifts', idx, 'capacity', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
updatePassportArray('lifts', idx, 'speed', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
updatePassportArray('lifts', idx, 'factoryNumber', v)} isEditing={isEditing} className="text-right w-full"/>
updatePassportArray('lifts', idx, 'manufacturer', v)} isEditing={isEditing} className="text-right w-full"/>
updatePassportArray('lifts', idx, 'wearPercent', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
updatePassportArray('lifts', idx, 'status', v)} isEditing={isEditing} className="text-right w-full"/>
updatePassportArray('lifts', idx, 'lastMaintenanceDate', v)} isEditing={isEditing} type="date" className="text-right w-full"/>
updatePassportArray('lifts', idx, 'nextMaintenanceDate', v)} isEditing={isEditing} type="date" className="text-right w-full"/>
{isEditing && (
{ if (!e.target.files) return; const fileList = e.target.files; const newFiles = Array.from(fileList).map((file: File) => URL.createObjectURL(file as Blob)); const updatedLift = { ...lift, documents: [...(lift.documents || []), ...newFiles] }; updatePassportArray('lifts', idx, 'documents', updatedLift.documents); }} className="text-xs" /> {lift.documents && lift.documents.length > 0 && (
{lift.documents.map((doc, docIdx) => (
Документ {docIdx + 1}
))}
)}
)} {!isEditing && lift.documents && lift.documents.length > 0 && (
{lift.documents.map((doc, docIdx) => ( Документ {docIdx + 1} ))}
)}
))} {isEditing && ( )}
{/* Глобальные поля (для всех домов) */} {globalFields.land && Object.keys(globalFields.land).map(key => { const globalField = globalFields.land[key]; const localField = land.customFields?.[key] || globalField; return (
updateCustomField('land', key, localField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {/* Локальные кастомные поля (только для этого дома) */} {land.customFields && Object.keys(land.customFields) .filter(key => !(globalFields.land && globalFields.land[key])) .map(key => { const customField = land.customFields![key]; return (
updateCustomField('land', key, customField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {isEditing && ( )}
{/* Глобальные поля (для всех домов) */} {globalFields.management && Object.keys(globalFields.management).map(key => { const globalField = globalFields.management[key]; const localField = management.customFields?.[key] || globalField; return (
updateCustomField('management', key, localField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {/* Локальные кастомные поля (только для этого дома) */} {management.customFields && Object.keys(management.customFields) .filter(key => !(globalFields.management && globalFields.management[key])) .map(key => { const customField = management.customFields![key]; return (
updateCustomField('management', key, customField.value, files)} isCustomField={true} customFieldKey={key} isEditing={isEditing} onValueChange={handleFieldValueChange} /> {isEditing && ( )}
); })} {isEditing && ( )}
{/* NEW SECTION: SERVICE CONTRACTS (RSO, INTERNET, ETC) */}
{serviceContracts.map((contract) => { const SvcIcon = getServiceIcon(contract.serviceType); const nextExp = contract.expiryDate ? new Date(contract.expiryDate) : null; const isExpiring = nextExp && nextExp < new Date(new Date().setMonth(new Date().getMonth() + 1)); return (
handleUpdateContract(contract.id, 'serviceType', v)} isEditing={isEditing} className="text-sm font-bold text-slate-800" />
handleUpdateContract(contract.id, 'contractNumber', v)} isEditing={isEditing} className="text-[10px] font-bold text-slate-500" />
{isEditing && ( )}
handleUpdateContract(contract.id, 'providerName', v)} isEditing={isEditing} className="text-xs font-bold text-slate-700" />
handleUpdateContract(contract.id, 'contractDate', v)} isEditing={isEditing} type="date" className="text-xs font-medium text-slate-600" />
{contract.expiryDate && (
{contract.expiryDate}
)}
); })} {isEditing && ( )}
); };