Files
mkd/components/building/PassportView.tsx
2026-02-04 00:17:04 +05:00

1872 lines
118 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};