Files
mkd/components/building/PassportView.tsx

1872 lines
118 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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<string, string> = {
'Heat': 'Гкал',
'Water': 'м³',
'Electricity': 'кВт⋅ч',
'Gas': 'м³',
'Other': ''
};
const defaultNames: Record<string, string> = {
'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 (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div className="bg-white rounded-2xl w-full max-w-lg p-6 shadow-2xl animate-slide-up max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-bold text-slate-800">Добавить прибор учета</h3>
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full">
<X className="w-5 h-5 text-slate-500"/>
</button>
</div>
<div className="space-y-4">
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Тип ресурса *</label>
<select
value={resource}
onChange={(e) => handleResourceChange(e.target.value as typeof resource)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="Heat">Отопление</option>
<option value="Water">Водоснабжение</option>
<option value="Electricity">Электроснабжение</option>
<option value="Gas">Газоснабжение</option>
<option value="Other">Другой</option>
</select>
</div>
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Название прибора *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={defaultNames[resource] || 'Введите название'}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Номер прибора *</label>
<input
type="text"
value={number}
onChange={(e) => setNumber(e.target.value)}
placeholder="Введите номер"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Модель</label>
<input
type="text"
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="Модель"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Производитель</label>
<input
type="text"
value={manufacturer}
onChange={(e) => setManufacturer(e.target.value)}
placeholder="Производитель"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
</div>
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Единица измерения</label>
<input
type="text"
value={unit}
onChange={(e) => setUnit(e.target.value)}
placeholder={defaultUnits[resource] || 'Единица измерения'}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Дата установки</label>
<input
type="date"
value={installDate}
onChange={(e) => setInstallDate(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Текущее показание</label>
<input
type="number"
step="0.01"
value={currentReading}
onChange={(e) => setCurrentReading(e.target.value)}
placeholder="0"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Последняя поверка</label>
<input
type="date"
value={lastVerification}
onChange={(e) => setLastVerification(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-slate-600 block mb-2">Следующая поверка</label>
<input
type="date"
value={nextVerification}
onChange={(e) => setNextVerification(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
</div>
<div className="flex gap-3 pt-4 border-t border-slate-200">
<button
onClick={handleSubmit}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors"
>
Добавить
</button>
<button
onClick={onClose}
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg font-bold text-sm hover:bg-slate-50 transition-colors"
>
Отмена
</button>
</div>
</div>
</div>
</div>
);
};
// Модальное окно карточки ПУ
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 (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm animate-fade-in" onClick={onClose}>
<div className="bg-white rounded-2xl w-full max-w-2xl p-6 shadow-2xl animate-slide-up max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-xl ${colorClass.replace('bg-', 'bg-').replace('text-', 'text-')}`}>
<Icon className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-xl font-bold text-slate-800">{label}</h3>
<p className="text-sm text-slate-500"> {meter.number || 'Не указан'} {meter.model && `${meter.model}`}</p>
</div>
</div>
<div className="flex gap-2">
{isEditing && (
<button
onClick={() => setIsEditingCard(!isEditingCard)}
className="p-2 hover:bg-slate-100 rounded-full"
title={isEditingCard ? "Сохранить" : "Редактировать"}
>
{isEditingCard ? <Save className="w-5 h-5 text-primary-600"/> : <Edit2 className="w-5 h-5 text-slate-500"/>}
</button>
)}
<button onClick={onClose} className="p-2 hover:bg-slate-100 rounded-full">
<X className="w-5 h-5 text-slate-500"/>
</button>
</div>
</div>
<div className="space-y-6">
{/* Основная информация */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-xs text-slate-500 block mb-1">Тип ресурса</label>
{isEditingCard ? (
<select
value={meter.resource}
onChange={(e) => onUpdate(meterIndex, 'resource', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="Heat">Отопление</option>
<option value="Water">Водоснабжение</option>
<option value="Electricity">Электроснабжение</option>
<option value="Gas">Газоснабжение</option>
<option value="Other">Другой</option>
</select>
) : (
<p className="font-bold text-slate-800">{label}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Название</label>
{isEditingCard ? (
<input
type="text"
value={meter.name || ''}
onChange={(e) => onUpdate(meterIndex, 'name', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{meter.name || label}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Номер прибора</label>
{isEditingCard ? (
<input
type="text"
value={meter.number || ''}
onChange={(e) => onUpdate(meterIndex, 'number', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{meter.number || 'Не указан'}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Модель</label>
{isEditingCard ? (
<input
type="text"
value={meter.model || ''}
onChange={(e) => onUpdate(meterIndex, 'model', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{meter.model || 'Не указана'}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Производитель</label>
{isEditingCard ? (
<input
type="text"
value={meter.manufacturer || ''}
onChange={(e) => onUpdate(meterIndex, 'manufacturer', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{meter.manufacturer || 'Не указан'}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Дата установки</label>
{isEditingCard ? (
<input
type="date"
value={meter.installDate || ''}
onChange={(e) => onUpdate(meterIndex, 'installDate', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{meter.installDate || 'Не указана'}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Последняя поверка</label>
{isEditingCard ? (
<input
type="date"
value={meter.lastVerification || ''}
onChange={(e) => onUpdate(meterIndex, 'lastVerification', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{meter.lastVerification || 'Не указана'}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Следующая поверка</label>
{isEditingCard ? (
<input
type="date"
value={meter.nextVerification || ''}
onChange={(e) => onUpdate(meterIndex, 'nextVerification', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className={`font-bold ${meter.nextVerification && new Date(meter.nextVerification) < new Date() ? 'text-red-600' : 'text-slate-800'}`}>
{meter.nextVerification || 'Не указана'}
</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Единица измерения</label>
{isEditingCard ? (
<input
type="text"
value={meter.unit || ''}
onChange={(e) => onUpdate(meterIndex, 'unit', e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800">{unit}</p>
)}
</div>
<div>
<label className="text-xs text-slate-500 block mb-1">Текущее показание ({unit})</label>
{isEditingCard ? (
<input
type="number"
step="0.01"
value={meter.currentReading || ''}
onChange={(e) => onUpdate(meterIndex, 'currentReading', parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
) : (
<p className="font-bold text-slate-800 text-lg">{meter.currentReading?.toLocaleString('ru-RU') || 'Не указано'}</p>
)}
</div>
</div>
{/* Статистика */}
{stats.count > 0 && (
<div className="border-t border-slate-200 pt-4">
<h4 className="font-bold text-sm text-slate-800 flex items-center gap-2 mb-4">
<BarChart3 className="w-4 h-4" /> Статистика показаний
</h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Общее потребление</p>
<p className="font-bold text-lg text-slate-800">{stats.total.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Среднее потребление</p>
<p className="font-bold text-lg text-slate-800">{stats.average.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Максимальное</p>
<p className="font-bold text-lg text-slate-800">{stats.max.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg">
<p className="text-xs text-slate-500 mb-1">Минимальное</p>
<p className="font-bold text-lg text-slate-800">{stats.min.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg col-span-2">
<p className="text-xs text-slate-500 mb-1">Период</p>
<p className="font-bold text-sm text-slate-800">
{stats.period.start && stats.period.end
? `${stats.period.start.toLocaleDateString('ru-RU')} - ${stats.period.end.toLocaleDateString('ru-RU')}`
: 'Не указан'}
</p>
<p className="text-xs text-slate-500 mt-1">Всего записей: {stats.count}</p>
</div>
</div>
</div>
)}
{/* История показаний */}
<div className="border-t border-slate-200 pt-4">
<div className="flex justify-between items-center mb-4">
<h4 className="font-bold text-sm text-slate-800 flex items-center gap-2">
<History className="w-4 h-4" /> История показаний
</h4>
{isEditingCard && (
<div className="flex gap-2">
<input
type="date"
value={newReadingDate}
onChange={(e) => setNewReadingDate(e.target.value)}
className="px-2 py-1 border border-slate-300 rounded text-xs"
placeholder="Дата"
/>
<input
type="number"
step="0.01"
value={newReadingValue}
onChange={(e) => setNewReadingValue(e.target.value)}
className="px-2 py-1 border border-slate-300 rounded text-xs w-24"
placeholder="Показание"
/>
<button
onClick={handleAddReading}
className="px-3 py-1 bg-primary-600 text-white rounded text-xs hover:bg-primary-700"
>
Добавить
</button>
</div>
)}
</div>
{history.length > 0 ? (
<div className="space-y-2 max-h-64 overflow-y-auto">
{history.map((reading, idx) => (
<div key={idx} className="flex justify-between items-center p-2 bg-slate-50 rounded text-xs">
<div className="flex items-center gap-2">
<Calendar className="w-3 h-3 text-slate-400" />
<span className="text-slate-600">{new Date(reading.date).toLocaleDateString('ru-RU')}</span>
</div>
<div className="flex items-center gap-4">
<span className="font-bold text-slate-800">{reading.value?.toLocaleString('ru-RU', { maximumFractionDigits: 2 }) || 0} {unit}</span>
{reading.consumption !== undefined && reading.consumption > 0 && (
<span className="text-slate-500 flex items-center gap-1">
<TrendingUp className="w-3 h-3" />
{reading.consumption.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit}
</span>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-xs text-slate-400 text-center py-4">Нет данных о показаниях. Добавьте первое показание.</p>
)}
</div>
{/* График потребления (только реальные данные) */}
{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 ? (
<div className="border-t border-slate-200 pt-4">
<h4 className="font-bold text-xs text-slate-500 uppercase mb-4 flex items-center gap-1">
<BarChart3 className="w-4 h-4"/> Потребление за период ({unit})
</h4>
<div className="h-32 flex items-end justify-between gap-1 pb-1 bg-slate-50 p-2 rounded">
{readingsWithConsumption.slice(0, 12).reverse().map((r, i) => (
<div key={i} className="flex flex-col items-center flex-1 group relative h-full justify-end">
<div
className={`w-full max-w-[20px] rounded-t min-h-[4px] transition-all hover:opacity-80 ${colorClass}`}
style={{ height: `${((r.consumption || 0) / maxConsumption) * 100}%` }}
title={`${new Date(r.date).toLocaleDateString('ru-RU')}: ${(r.consumption || 0).toLocaleString('ru-RU')} ${unit}`}
></div>
</div>
))}
</div>
</div>
) : null;
})()}
{/* Документы */}
{isEditingCard && (
<div className="border-t border-slate-200 pt-4">
<label className="text-xs text-slate-500 block mb-2">Документы</label>
<input
type="file"
multiple
onChange={(e) => {
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 && (
<div className="mt-2 space-y-1">
{meter.documents.map((doc, docIdx) => (
<div key={docIdx} className="flex items-center gap-2 text-xs text-slate-600 bg-slate-50 p-1.5 rounded">
<File className="w-3 h-3" />
<span className="flex-1 truncate">Документ {docIdx + 1}</span>
<button
onClick={() => {
const updatedDocuments = meter.documents!.filter((_, i) => i !== docIdx);
onUpdate(meterIndex, 'documents', updatedDocuments);
}}
className="text-red-500 hover:text-red-700"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
// Вынесенный компонент поля паспорта — стабильная идентичность предотвращает потерю фокуса при вводе
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<HTMLInputElement>) => {
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 (
<div>
<label className="block text-xs font-bold text-slate-500 mb-1">{label}</label>
<EditableField
value={value || ''}
onChange={handleValueChange}
isEditing={isEditing}
type={type as any}
className="text-sm text-slate-800 font-medium"
/>
{isEditing && onFilesChange && (
<div className="mt-2">
<label className="flex items-center gap-2 text-xs text-primary-600 cursor-pointer hover:text-primary-700">
<Upload className="w-3 h-3" />
<span className="font-bold">Загрузить файл</span>
<input
type="file"
multiple
onChange={handleFileUpload}
className="hidden"
/>
</label>
{files && files.length > 0 && (
<div className="mt-2 space-y-1">
{files.map((file, idx) => (
<div key={idx} className="flex items-center gap-2 text-xs text-slate-600 bg-slate-50 p-1.5 rounded">
<File className="w-3 h-3" />
<span className="flex-1 truncate">{file}</span>
<button
onClick={() => onFilesChange(files.filter((_, i) => i !== idx))}
className="text-red-500 hover:text-red-700"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
)}
</div>
)}
{!isEditing && files && files.length > 0 && (
<div className="mt-2 space-y-1">
{files.map((file, idx) => (
<a key={idx} href={file} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs text-primary-600 hover:text-primary-700">
<File className="w-3 h-3" />
<span className="truncate">Файл {idx + 1}</span>
</a>
))}
</div>
)}
</div>
);
};
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 }) => (
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm mb-4 relative">
<h3 className="font-bold text-slate-800 text-sm mb-3 border-b border-slate-100 pb-2 flex items-center justify-between relative">
<span className="flex items-center gap-2">
{Icon && <Icon className="w-4 h-4 text-primary-500"/>}
{title}
</span>
{isEditing && (
<div className="absolute top-0 right-0">
<Edit2 className="w-4 h-4 text-primary-600" title="Режим редактирования" />
</div>
)}
</h3>
<div className={noGrid ? "" : "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"}>
{children}
</div>
</div>
);
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 (
<div className="animate-fade-in pb-10">
{showAddFieldModal && (
<AddCustomFieldModal
isOpen={showAddFieldModal}
onClose={() => {
setShowAddFieldModal(false);
setCurrentSection(null);
}}
onConfirm={handleConfirmAddField}
section={currentSection || undefined}
/>
)}
<Section title="1. Общие сведения" icon={FileText}>
<PassportField label="Адрес (ФИАС)" value={general.address} section="general" fieldKey="address" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Код ФИАС" value={general.fiasCode} section="general" fieldKey="fiasCode" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Год постройки" value={general.constructionYear} section="general" fieldKey="constructionYear" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Год ввода" value={general.commissionYear} section="general" fieldKey="commissionYear" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Последний кап.ремонт" value={general.lastRepairYear || ''} section="general" fieldKey="lastRepairYear" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Серия проекта" value={general.seriesType} section="general" fieldKey="seriesType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Этажность" value={general.floors} section="general" fieldKey="floors" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Подземных этажей" value={general.undergroundFloors} section="general" fieldKey="undergroundFloors" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Кадастровый № (Дом)" value={general.cadastralNumberBuild} section="general" fieldKey="cadastralNumberBuild" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Кадастровый № (Земля)" value={general.cadastralNumberLand} section="general" fieldKey="cadastralNumberLand" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Общая площадь (м²)" value={general.totalArea} section="general" fieldKey="totalArea" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Жилая площадь (м²)" value={general.livingArea} section="general" fieldKey="livingArea" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Нежилая площадь (м²)" value={general.nonLivingArea} section="general" fieldKey="nonLivingArea" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="МОП (м²)" value={general.commonArea} section="general" fieldKey="commonArea" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Новые поля */}
<PassportField label="Кол-во подъездов" value={general.entrancesCount || ''} section="general" fieldKey="entrancesCount" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Разноэтажность" value={general.hasDifferentFloors || false} section="general" fieldKey="hasDifferentFloors" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Кол-во офисов" value={general.officesCount || ''} section="general" fieldKey="officesCount" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Вычисляемые поля из лицевых счетов */}
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
<label className="block text-xs font-bold text-slate-500 mb-2">Данные из лицевых счетов:</label>
<div className="space-y-1 text-xs">
<div className="flex justify-between"><span className="text-slate-600">Жилых лицевых счетов:</span><span className="font-bold text-slate-800">{accountsStats.residentialAccountsCount}</span></div>
<div className="flex justify-between"><span className="text-slate-600">Нежилых лицевых счетов:</span><span className="font-bold text-slate-800">{accountsStats.nonResidentialAccountsCount}</span></div>
<div className="flex justify-between"><span className="text-slate-600">Офисов (из счетов):</span><span className="font-bold text-slate-800">{accountsStats.officesAccountsCount}</span></div>
<div className="flex justify-between"><span className="text-slate-600">Квартир (из счетов):</span><span className="font-bold text-slate-800">{accountsStats.apartmentsCount}</span></div>
<div className="flex justify-between"><span className="text-slate-600">Площадь нежилых в ОДИ (м²):</span><span className="font-bold text-slate-800">{accountsStats.nonLivingCommonArea.toFixed(2)}</span></div>
</div>
</div>
{/* Глобальные поля (для всех домов) */}
{globalFields.general && Object.keys(globalFields.general).map(key => {
const globalField = globalFields.general[key];
const localField = general.customFields?.[key] || globalField;
const isGlobal = true;
return (
<div key={`global-${key}`} className="relative group">
<PassportField
label={key}
value={localField.value}
section="general"
fieldKey={`customFields.${key}.value`}
type={localField.type as any}
files={localField.files}
onFilesChange={(files) => updateCustomField('general', key, localField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
if (confirm(`Удалить глобальное поле "${key}" из всех домов?`)) {
storageService.deleteGlobalPassportField('general', key);
const sectionData = building.passport.general as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('general', 'customFields', customFields);
}
}}
className="absolute top-1 right-1 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить глобальное поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{/* Локальные кастомные поля (только для этого дома) */}
{general.customFields && Object.keys(general.customFields)
.filter(key => !(globalFields.general && globalFields.general[key]))
.map(key => {
const customField = general.customFields![key];
return (
<div key={key} className="relative group">
<PassportField
label={key}
value={customField.value}
section="general"
fieldKey={`customFields.${key}.value`}
type={customField.type as any}
files={customField.files}
onFilesChange={(files) => updateCustomField('general', key, customField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
const sectionData = building.passport.general as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('general', 'customFields', customFields);
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{isEditing && (
<button
onClick={() => addCustomField('general')}
className="border-2 border-dashed border-slate-300 rounded-lg p-3 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить поле
</button>
)}
</Section>
<Section title="2. Конструктивные элементы" icon={Layers}>
<PassportField label="Фундамент (Тип)" value={construction.foundationType} section="construction" fieldKey="foundationType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Стены" value={construction.wallMaterial} section="construction" fieldKey="wallMaterial" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Кровля (Тип)" value={construction.roofType} section="construction" fieldKey="roofType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Площадь кровли (м²)" value={construction.roofArea} section="construction" fieldKey="roofArea" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Фасад" value={construction.facadeType} section="construction" fieldKey="facadeType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Утепление фасада" value={construction.facadeInsulation} section="construction" fieldKey="facadeInsulation" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Новые поля */}
<PassportField label="Процент износа (%)" value={construction.wearPercent || ''} section="construction" fieldKey="wearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Подвал" value={construction.hasBasement || false} section="construction" fieldKey="hasBasement" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Тех этаж" value={construction.hasTechFloor || false} section="construction" fieldKey="hasTechFloor" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Чердак" value={construction.hasAttic || false} section="construction" fieldKey="hasAttic" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Цоколь" value={construction.hasBasementFloor || false} section="construction" fieldKey="hasBasementFloor" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Глобальные поля (для всех домов) */}
{globalFields.construction && Object.keys(globalFields.construction).map(key => {
const globalField = globalFields.construction[key];
const localField = construction.customFields?.[key] || globalField;
return (
<div key={`global-${key}`} className="relative group">
<PassportField
label={key}
value={localField.value}
section="construction"
fieldKey={`customFields.${key}.value`}
type={localField.type as any}
files={localField.files}
onFilesChange={(files) => updateCustomField('construction', key, localField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
if (confirm(`Удалить глобальное поле "${key}" из всех домов?`)) {
storageService.deleteGlobalPassportField('construction', key);
const sectionData = building.passport.construction as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('construction', 'customFields', customFields);
}
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить глобальное поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{/* Локальные кастомные поля (только для этого дома) */}
{construction.customFields && Object.keys(construction.customFields)
.filter(key => !(globalFields.construction && globalFields.construction[key]))
.map(key => {
const customField = construction.customFields![key];
return (
<div key={key} className="relative group">
<PassportField
label={key}
value={customField.value}
section="construction"
fieldKey={`customFields.${key}.value`}
type={customField.type as any}
files={customField.files}
onFilesChange={(files) => updateCustomField('construction', key, customField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
const sectionData = building.passport.construction as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('construction', 'customFields', customFields);
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{isEditing && (
<button
onClick={() => addCustomField('construction')}
className="border-2 border-dashed border-slate-300 rounded-lg p-3 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить поле
</button>
)}
</Section>
<Section title="3. Инженерные системы" icon={Zap}>
<PassportField label="Отопление" value={engineering.heatingType} section="engineering" fieldKey="heatingType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Износ отопления (%)" value={engineering.heatingWearPercent || ''} section="engineering" fieldKey="heatingWearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Наличие ИТП" value={engineering.hasITP} section="engineering" fieldKey="hasITP" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="ХВС (Материал)" value={engineering.waterSupplyMaterial} section="engineering" fieldKey="waterSupplyMaterial" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Износ ХВС (%)" value={engineering.waterSupplyWearPercent || ''} section="engineering" fieldKey="waterSupplyWearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Канализация" value={engineering.sewerMaterial} section="engineering" fieldKey="sewerMaterial" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Износ канализации (%)" value={engineering.sewerWearPercent || ''} section="engineering" fieldKey="sewerWearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Газоснабжение" value={engineering.gasType} section="engineering" fieldKey="gasType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Износ газоснабжения (%)" value={engineering.gasWearPercent || ''} section="engineering" fieldKey="gasWearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Эл. вводов" value={engineering.electricityEntries} section="engineering" fieldKey="electricityEntries" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Износ электроснабжения (%)" value={engineering.electricityWearPercent || ''} section="engineering" fieldKey="electricityWearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Вентиляция" value={engineering.ventilationType} section="engineering" fieldKey="ventilationType" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Износ вентиляции (%)" value={engineering.ventilationWearPercent || ''} section="engineering" fieldKey="ventilationWearPercent" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Глобальные поля (для всех домов) */}
{globalFields.engineering && Object.keys(globalFields.engineering).map(key => {
const globalField = globalFields.engineering[key];
const localField = engineering.customFields?.[key] || globalField;
return (
<div key={`global-${key}`} className="relative group">
<PassportField
label={key}
value={localField.value}
section="engineering"
fieldKey={`customFields.${key}.value`}
type={localField.type as any}
files={localField.files}
onFilesChange={(files) => updateCustomField('engineering', key, localField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
if (confirm(`Удалить глобальное поле "${key}" из всех домов?`)) {
storageService.deleteGlobalPassportField('engineering', key);
const sectionData = building.passport.engineering as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('engineering', 'customFields', customFields);
}
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить глобальное поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{/* Локальные кастомные поля (только для этого дома) */}
{engineering.customFields && Object.keys(engineering.customFields)
.filter(key => !(globalFields.engineering && globalFields.engineering[key]))
.map(key => {
const customField = engineering.customFields![key];
return (
<div key={key} className="relative group">
<PassportField
label={key}
value={customField.value}
section="engineering"
fieldKey={`customFields.${key}.value`}
type={customField.type as any}
files={customField.files}
onFilesChange={(files) => updateCustomField('engineering', key, customField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
const sectionData = building.passport.engineering as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('engineering', 'customFields', customFields);
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{isEditing && (
<button
onClick={() => addCustomField('engineering')}
className="border-2 border-dashed border-slate-300 rounded-lg p-3 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить поле
</button>
)}
</Section>
<Section title="4. Приборы учета (ОДПУ)" icon={Gauge}>
{/* Список всех ПУ */}
{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 (
<div
key={actualIdx}
onClick={() => setSelectedMeter({ meter, index: actualIdx })}
className="p-4 rounded-xl border border-slate-200 bg-white shadow-sm cursor-pointer hover:shadow-md transition-all"
>
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-3 flex-1">
<div className={`p-2 rounded-lg ${colorClass}`}>
<Icon className="w-5 h-5"/>
</div>
<div className="flex-1">
<p className="font-bold text-slate-800 text-sm">{label}</p>
{meter.number && (
<p className="text-xs text-slate-500"> {meter.number}</p>
)}
</div>
</div>
{isEditing && (
<button
onClick={(e) => {
e.stopPropagation();
if (confirm('Удалить этот прибор учета?')) {
const newMeters = meters.filter((_, i) => i !== actualIdx);
updatePassport('meters' as any, 'meters' as any, newMeters);
}
}}
className="text-red-500 hover:text-red-700 p-1"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
{meter.currentReading !== undefined && (
<div>
<span className="text-slate-500">Текущее показание:</span>
<p className="font-bold text-slate-800">{meter.currentReading.toLocaleString('ru-RU')} {unit}</p>
</div>
)}
{meter.nextVerification && (
<div>
<span className="text-slate-500">Поверка до:</span>
<p className={`font-bold ${new Date(meter.nextVerification) < new Date() ? 'text-red-600' : 'text-slate-800'}`}>
{new Date(meter.nextVerification).toLocaleDateString('ru-RU')}
</p>
</div>
)}
</div>
</div>
);
})}
{meters.filter(m => m.hasMeter).length === 0 && (
<p className="text-sm text-slate-400 text-center py-8">Нет добавленных приборов учета</p>
)}
{isEditing && (
<button
onClick={() => setShowAddMeterModal(true)}
className="border-2 border-dashed border-slate-300 rounded-lg p-3 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить ПУ
</button>
)}
</Section>
{/* Модальное окно создания ПУ */}
<AddMeterModal
isOpen={showAddMeterModal}
onClose={() => 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 && (
<MeterCardModal
meter={selectedMeter.meter}
meterIndex={selectedMeter.index}
isEditing={isEditing}
onClose={() => 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);
}}
/>
)}
<Section title="5. Лифтовое хозяйство" icon={ArrowUpFromLine}>
{lifts.map((lift, idx) => (
<div key={idx} className="p-4 border border-slate-200 rounded-xl bg-white shadow-sm">
<div className="flex justify-between items-start mb-3">
<p className="font-bold text-slate-800 text-sm">Лифт {idx + 1}</p>
{isEditing && (
<button
onClick={() => {
const newLifts = lifts.filter((_, i) => i !== idx);
updatePassport('lifts' as any, 'lifts' as any, newLifts);
}}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<label className="text-slate-500 block mb-1">Тип:</label>
<EditableField value={lift.type || ''} onChange={(v) => updatePassportArray('lifts', idx, 'type', v)} isEditing={isEditing} className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Год установки:</label>
<EditableField value={lift.installYear || ''} onChange={(v) => updatePassportArray('lifts', idx, 'installYear', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Грузоподъемность (кг):</label>
<EditableField value={lift.capacity || ''} onChange={(v) => updatePassportArray('lifts', idx, 'capacity', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Скорость (м/с):</label>
<EditableField value={lift.speed || ''} onChange={(v) => updatePassportArray('lifts', idx, 'speed', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Заводской номер:</label>
<EditableField value={lift.factoryNumber || ''} onChange={(v) => updatePassportArray('lifts', idx, 'factoryNumber', v)} isEditing={isEditing} className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Производитель:</label>
<EditableField value={lift.manufacturer || ''} onChange={(v) => updatePassportArray('lifts', idx, 'manufacturer', v)} isEditing={isEditing} className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Износ (%):</label>
<EditableField value={lift.wearPercent || ''} onChange={(v) => updatePassportArray('lifts', idx, 'wearPercent', Number(v))} isEditing={isEditing} type="number" className="text-right w-full"/>
</div>
<div>
<label className="text-slate-500 block mb-1">Статус:</label>
<EditableField value={lift.status || ''} onChange={(v) => updatePassportArray('lifts', idx, 'status', v)} isEditing={isEditing} className="text-right w-full"/>
</div>
<div className="col-span-2">
<label className="text-slate-500 block mb-1">Последнее обслуживание:</label>
<EditableField value={lift.lastMaintenanceDate || ''} onChange={(v) => updatePassportArray('lifts', idx, 'lastMaintenanceDate', v)} isEditing={isEditing} type="date" className="text-right w-full"/>
</div>
<div className="col-span-2">
<label className="text-slate-500 block mb-1">Следующее обслуживание:</label>
<EditableField value={lift.nextMaintenanceDate || ''} onChange={(v) => updatePassportArray('lifts', idx, 'nextMaintenanceDate', v)} isEditing={isEditing} type="date" className="text-right w-full"/>
</div>
{isEditing && (
<div className="col-span-2">
<label className="text-slate-500 block mb-1">Документы:</label>
<input
type="file"
multiple
onChange={(e) => {
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 && (
<div className="mt-2 space-y-1">
{lift.documents.map((doc, docIdx) => (
<div key={docIdx} className="flex items-center gap-2 text-xs text-slate-600 bg-slate-50 p-1.5 rounded">
<File className="w-3 h-3" />
<span className="flex-1 truncate">Документ {docIdx + 1}</span>
<button
onClick={() => {
const updatedDocuments = lift.documents!.filter((_, i) => i !== docIdx);
updatePassportArray('lifts', idx, 'documents', updatedDocuments);
}}
className="text-red-500 hover:text-red-700"
>
<X className="w-3 h-3" />
</button>
</div>
))}
</div>
)}
</div>
)}
{!isEditing && lift.documents && lift.documents.length > 0 && (
<div className="col-span-2 space-y-1">
{lift.documents.map((doc, docIdx) => (
<a key={docIdx} href={doc} target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-xs text-primary-600 hover:text-primary-700">
<File className="w-3 h-3" />
<span>Документ {docIdx + 1}</span>
</a>
))}
</div>
)}
</div>
</div>
))}
{isEditing && (
<button
onClick={() => {
const newLift = {
count: 1,
type: '',
capacity: 0,
speed: 0,
installYear: new Date().getFullYear(),
factoryNumber: ''
};
updatePassport('lifts' as any, 'lifts' as any, [...lifts, newLift]);
}}
className="border-2 border-dashed border-slate-300 rounded-lg p-4 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить лифт
</button>
)}
</Section>
<Section title="6. Земля и благоустройство" icon={Trees}>
<PassportField label="Площадь участка (м²)" value={land.area} section="land" fieldKey="area" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Детская площадка" value={land.hasPlayground} section="land" fieldKey="hasPlayground" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Спортивная площадка" value={land.hasSportsGround} section="land" fieldKey="hasSportsGround" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Парковка" value={land.hasParking} section="land" fieldKey="hasParking" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Ограждение" value={land.hasFencing} section="land" fieldKey="hasFencing" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Площадка для контейнеров" value={land.hasContainerSite} section="land" fieldKey="hasContainerSite" type="checkbox" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Глобальные поля (для всех домов) */}
{globalFields.land && Object.keys(globalFields.land).map(key => {
const globalField = globalFields.land[key];
const localField = land.customFields?.[key] || globalField;
return (
<div key={`global-${key}`} className="relative group">
<PassportField
label={key}
value={localField.value}
section="land"
fieldKey={`customFields.${key}.value`}
type={localField.type as any}
files={localField.files}
onFilesChange={(files) => updateCustomField('land', key, localField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
if (confirm(`Удалить глобальное поле "${key}" из всех домов?`)) {
storageService.deleteGlobalPassportField('land', key);
const sectionData = building.passport.land as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('land', 'customFields', customFields);
}
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить глобальное поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{/* Локальные кастомные поля (только для этого дома) */}
{land.customFields && Object.keys(land.customFields)
.filter(key => !(globalFields.land && globalFields.land[key]))
.map(key => {
const customField = land.customFields![key];
return (
<div key={key} className="relative group">
<PassportField
label={key}
value={customField.value}
section="land"
fieldKey={`customFields.${key}.value`}
type={customField.type as any}
files={customField.files}
onFilesChange={(files) => updateCustomField('land', key, customField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
const sectionData = building.passport.land as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('land', 'customFields', customFields);
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{isEditing && (
<button
onClick={() => addCustomField('land')}
className="border-2 border-dashed border-slate-300 rounded-lg p-3 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить поле
</button>
)}
</Section>
<Section title="7. Управление" icon={Briefcase}>
<PassportField label="Дата договора" value={management.contractDate} section="management" fieldKey="contractDate" type="date" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Номер договора" value={management.contractNumber} section="management" fieldKey="contractNumber" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Тариф (содержание) ₽/м²" value={management.tariffMaintenance} section="management" fieldKey="tariffMaintenance" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
<PassportField label="Резервный фонд (%)" value={management.reserveFund || ''} section="management" fieldKey="reserveFund" type="number" isEditing={isEditing} onValueChange={handleFieldValueChange} />
{/* Глобальные поля (для всех домов) */}
{globalFields.management && Object.keys(globalFields.management).map(key => {
const globalField = globalFields.management[key];
const localField = management.customFields?.[key] || globalField;
return (
<div key={`global-${key}`} className="relative group">
<PassportField
label={key}
value={localField.value}
section="management"
fieldKey={`customFields.${key}.value`}
type={localField.type as any}
files={localField.files}
onFilesChange={(files) => updateCustomField('management', key, localField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
if (confirm(`Удалить глобальное поле "${key}" из всех домов?`)) {
storageService.deleteGlobalPassportField('management', key);
const sectionData = building.passport.management as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('management', 'customFields', customFields);
}
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить глобальное поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{/* Локальные кастомные поля (только для этого дома) */}
{management.customFields && Object.keys(management.customFields)
.filter(key => !(globalFields.management && globalFields.management[key]))
.map(key => {
const customField = management.customFields![key];
return (
<div key={key} className="relative group">
<PassportField
label={key}
value={customField.value}
section="management"
fieldKey={`customFields.${key}.value`}
type={customField.type as any}
files={customField.files}
onFilesChange={(files) => updateCustomField('management', key, customField.value, files)}
isCustomField={true}
customFieldKey={key}
isEditing={isEditing}
onValueChange={handleFieldValueChange}
/>
{isEditing && (
<button
onClick={() => {
const sectionData = building.passport.management as any;
const customFields = { ...(sectionData.customFields || {}) };
delete customFields[key];
updatePassport('management', 'customFields', customFields);
}}
className="absolute top-0 right-0 p-1 text-red-500 hover:text-red-700 opacity-0 group-hover:opacity-100 transition-opacity"
title="Удалить поле"
>
<X className="w-3 h-3" />
</button>
)}
</div>
);
})}
{isEditing && (
<button
onClick={() => addCustomField('management')}
className="border-2 border-dashed border-slate-300 rounded-lg p-3 text-xs font-bold text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" /> Добавить поле
</button>
)}
</Section>
{/* NEW SECTION: SERVICE CONTRACTS (RSO, INTERNET, ETC) */}
<Section title="8. Договоры с РСО и подрядчиками" icon={ShieldCheck} noGrid>
<div className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{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 (
<div key={contract.id} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm group hover:border-primary-300 transition-colors relative">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-100 text-slate-500 rounded-lg group-hover:bg-primary-50 group-hover:text-primary-600 transition-colors">
<SvcIcon className="w-5 h-5"/>
</div>
<div>
<EditableField
value={contract.serviceType}
onChange={(v) => handleUpdateContract(contract.id, 'serviceType', v)}
isEditing={isEditing}
className="text-sm font-bold text-slate-800"
/>
<div className="flex items-center gap-1 mt-0.5">
<span className="text-[10px] text-slate-400 font-bold"></span>
<EditableField
value={contract.contractNumber}
onChange={(v) => handleUpdateContract(contract.id, 'contractNumber', v)}
isEditing={isEditing}
className="text-[10px] font-bold text-slate-500"
/>
</div>
</div>
</div>
{isEditing && (
<button
onClick={() => handleRemoveContract(contract.id)}
className="p-1 text-slate-300 hover:text-red-500 transition-colors"
>
<Trash2 className="w-4 h-4"/>
</button>
)}
</div>
<div className="space-y-2 mt-2 pt-3 border-t border-slate-100">
<div>
<label className="text-[9px] text-slate-400 font-bold uppercase block">Контрагент</label>
<EditableField
value={contract.providerName}
onChange={(v) => handleUpdateContract(contract.id, 'providerName', v)}
isEditing={isEditing}
className="text-xs font-bold text-slate-700"
/>
</div>
<div className="flex justify-between items-end">
<div>
<label className="text-[9px] text-slate-400 font-bold uppercase block">Дата заключения</label>
<EditableField
value={contract.contractDate}
onChange={(v) => handleUpdateContract(contract.id, 'contractDate', v)}
isEditing={isEditing}
type="date"
className="text-xs font-medium text-slate-600"
/>
</div>
{contract.expiryDate && (
<div className="text-right">
<label className="text-[9px] text-slate-400 font-bold uppercase block">Действует до</label>
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded ${isExpiring ? 'bg-red-50 text-red-600' : 'bg-emerald-50 text-emerald-600'}`}>
{contract.expiryDate}
</span>
</div>
)}
</div>
</div>
</div>
);
})}
{isEditing && (
<button
onClick={handleAddContract}
className="border-2 border-dashed border-slate-200 rounded-xl p-6 flex flex-col items-center justify-center text-slate-400 hover:border-primary-400 hover:text-primary-500 transition-all bg-slate-50/50"
>
<Plus className="w-6 h-6 mb-2"/>
<span className="text-xs font-bold uppercase tracking-wider">Добавить договор</span>
</button>
)}
</div>
</div>
</Section>
</div>
);
};