import React, { useState, useMemo, useEffect, useRef } from 'react'; import { Building, PersonalAccount, AccountMeter, MeterReading, Debtor, ResidentProfile, RegisteredPerson } from '../../types'; import { backendApi } from '../../services/apiClient'; import { REFRESH_EVENTS } from '../../constants/refreshEvents'; import { ArrowLeft, Building2, User, Users, FileCheck, CheckCircle2, X, Droplets, Flame, Zap, Gauge, ChevronRight, BarChart3, History, Search, AlertCircle, Phone, Calendar, Mail, MessageSquare, Edit2, Save, AlertTriangle, Heart, Smile, Frown, Shield, FileText, Plus, Trash2, UserPlus, Upload, File, TrendingUp, Clock } from 'lucide-react'; // Компонент для редактирования номера лицевого счета с сохранением курсора const AccountNumberInput: React.FC<{ value: string; onChange: (value: string) => void }> = ({ value, onChange }) => { const [localValue, setLocalValue] = useState(value); const inputRef = useRef(null); const wasFocused = useRef(false); useEffect(() => { setLocalValue(value); }, [value]); useEffect(() => { if (wasFocused.current && inputRef.current) { const cursorPosition = inputRef.current.selectionStart || 0; inputRef.current.focus(); inputRef.current.setSelectionRange(cursorPosition, cursorPosition); wasFocused.current = false; } }); const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setLocalValue(newValue); wasFocused.current = true; onChange(newValue); }; const handleFocus = () => { wasFocused.current = true; }; return ( ); }; // Компонент для редактирования числовых полей с сохранением курсора const EditableNumberInput: React.FC<{ value: number | string; onChange: (value: number) => void; className?: string; placeholder?: string; step?: string; }> = ({ value, onChange, className = "", placeholder, step }) => { const [localValue, setLocalValue] = useState(String(value || '')); const inputRef = useRef(null); const wasFocused = useRef(false); useEffect(() => { setLocalValue(String(value || '')); }, [value]); useEffect(() => { if (wasFocused.current && inputRef.current) { inputRef.current.focus(); // setSelectionRange не поддерживается для type="number" wasFocused.current = false; } }); const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setLocalValue(newValue); wasFocused.current = true; const numValue = step ? parseFloat(newValue) || 0 : parseInt(newValue) || 0; setTimeout(() => { onChange(numValue); }, 0); }; const handleFocus = () => { wasFocused.current = true; }; return ( ); }; // Компонент для редактирования текстовых полей с сохранением курсора const EditableTextInput: React.FC<{ value: string; onChange: (value: string) => void; className?: string; placeholder?: string; }> = ({ value, onChange, className = "", placeholder }) => { const [localValue, setLocalValue] = useState(value || ''); const inputRef = useRef(null); const wasFocused = useRef(false); useEffect(() => { setLocalValue(value || ''); }, [value]); useEffect(() => { if (wasFocused.current && inputRef.current) { const cursorPosition = inputRef.current.selectionStart || 0; inputRef.current.focus(); inputRef.current.setSelectionRange(cursorPosition, cursorPosition); wasFocused.current = false; } }); const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setLocalValue(newValue); wasFocused.current = true; setTimeout(() => { onChange(newValue); }, 0); }; const handleFocus = () => { wasFocused.current = true; }; return ( ); }; // Модальное окно для создания нового индивидуального ПУ const AddAccountMeterModal: React.FC<{ isOpen: boolean; account: PersonalAccount; onClose: () => void; onAdd: (meter: AccountMeter) => void; }> = ({ isOpen, account, onClose, onAdd }) => { const [type, setType] = useState<'ХВС' | 'ГВС' | 'Э/Э' | 'Газ' | 'Other'>('ХВС'); const [name, setName] = useState(''); const [number, setNumber] = useState(''); const [make, setMake] = 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 = { 'ХВС': 'м³', 'ГВС': 'м³', 'Э/Э': 'кВт⋅ч', 'Газ': 'м³', 'Other': '' }; const defaultNames: Record = { 'ХВС': 'Холодная вода', 'ГВС': 'Горячая вода', 'Э/Э': 'Электричество', 'Газ': 'Газоснабжение', 'Other': '' }; const handleTypeChange = (newType: typeof type) => { setType(newType); if (!name || name === defaultNames[type]) { setName(defaultNames[newType] || ''); } if (!unit || unit === defaultUnits[type]) { setUnit(defaultUnits[newType] || ''); } }; const handleSubmit = () => { if (!number.trim() || !make.trim()) { alert('Заполните номер и модель прибора'); return; } const newMeter: AccountMeter = { id: `meter-${Date.now()}`, type, make: make.trim(), number: number.trim(), name: name.trim() || defaultNames[type] || undefined, manufacturer: manufacturer.trim() || undefined, unit: unit.trim() || defaultUnits[type] || undefined, installDate: installDate || undefined, lastVerification: lastVerification || new Date().toISOString().split('T')[0], nextVerification: nextVerification || new Date(new Date().setFullYear(new Date().getFullYear() + 4)).toISOString().split('T')[0], currentReading: currentReading ? parseFloat(currentReading) : undefined, readings: currentReading ? [{ date: new Date().toISOString().split('T')[0], value: parseFloat(currentReading), consumption: 0, source: 'manual' }] : [], documents: [] }; onAdd(newMeter); // Сброс формы setType('ХВС'); setName(''); setNumber(''); setMake(''); setManufacturer(''); setUnit(''); setInstallDate(''); setLastVerification(''); setNextVerification(''); setCurrentReading(''); onClose(); }; if (!isOpen) return null; return (
e.stopPropagation()}>

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

Кв. {account.apartmentNumber}

setName(e.target.value)} placeholder={defaultNames[type] || 'Введите название'} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setNumber(e.target.value)} placeholder="Введите номер" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setMake(e.target.value)} placeholder="Модель" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setManufacturer(e.target.value)} placeholder="Производитель" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setUnit(e.target.value)} placeholder={defaultUnits[type] || 'Единица измерения'} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setInstallDate(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setCurrentReading(e.target.value)} placeholder="0" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setLastVerification(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
setNextVerification(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
); }; // Sub-component: Add Reading Modal const AddReadingModal: React.FC<{ meter: AccountMeter; onClose: () => void; onAdd: (reading: MeterReading) => void; }> = ({ meter, onClose, onAdd }) => { const [formData, setFormData] = useState({ date: new Date().toISOString().split('T')[0], value: '', consumption: '', source: 'manual' as 'app' | 'manual' | 'iot', }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!formData.value || !formData.consumption) { alert('Заполните показание и расход'); return; } const reading: MeterReading = { date: formData.date, value: parseFloat(formData.value), consumption: parseFloat(formData.consumption), source: formData.source, }; onAdd(reading); onClose(); }; return (
e.stopPropagation()}>

Добавить показание

{meter.type} • № {meter.number}

setFormData({ ...formData, date: e.target.value })} className="w-full p-3 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none" />
setFormData({ ...formData, value: e.target.value })} placeholder="Текущее показание" className="w-full p-3 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none" />
setFormData({ ...formData, consumption: e.target.value })} placeholder="Расход за период" className="w-full p-3 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none" />
); }; // Sub-component: Meter Modal const MeterHistoryModal: React.FC<{ meter: AccountMeter, onClose: () => void, onAddReading?: (reading: MeterReading) => void }> = ({ meter, onClose, onAddReading }) => { const [showAddModal, setShowAddModal] = useState(false); const handleAddReading = (reading: MeterReading) => { if (onAddReading) { onAddReading(reading); } }; // Сортируем показания по дате (от новых к старым) const sortedReadings = useMemo(() => { return [...meter.readings].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); }, [meter.readings]); // Статистика по показаниям const stats = useMemo(() => { if (!sortedReadings || sortedReadings.length === 0) { return { total: 0, average: 0, max: 0, min: 0, count: 0, period: { start: null, end: null } }; } const readingsWithConsumption = sortedReadings.filter(r => r.consumption !== undefined && r.consumption > 0); if (readingsWithConsumption.length === 0) { return { total: 0, average: 0, max: 0, min: 0, count: sortedReadings.length, period: sortedReadings.length > 0 ? { start: new Date(Math.min(...sortedReadings.map(r => new Date(r.date).getTime()))), end: new Date(Math.max(...sortedReadings.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: sortedReadings.length, period: { start: new Date(Math.min(...dates.map(d => d.getTime()))), end: new Date(Math.max(...dates.map(d => d.getTime()))) } }; }, [sortedReadings]); // Calculate max value for simple graph scaling const maxVal = Math.max(...sortedReadings.map(r => r.consumption || 0), 1); const unit = meter.unit || (meter.type === 'Э/Э' ? 'кВт⋅ч' : 'м³'); // Config based on type let colorClass = 'bg-slate-500'; let textClass = 'text-slate-600'; let bgClass = 'bg-slate-50'; switch(meter.type) { case 'ГВС': colorClass = 'bg-red-500'; textClass = 'text-red-600'; bgClass = 'bg-red-50'; break; case 'ХВС': colorClass = 'bg-blue-500'; textClass = 'text-blue-600'; bgClass = 'bg-blue-50'; break; case 'Э/Э': colorClass = 'bg-amber-500'; textClass = 'text-amber-600'; bgClass = 'bg-amber-50'; break; } return (
e.stopPropagation()}>

{meter.type} Счетчик

Активен

№ {meter.number} • {meter.make}

{/* Основная информация */}
Текущее показание

{meter.currentReading?.toLocaleString('ru-RU', { maximumFractionDigits: 2 }) || (sortedReadings.length > 0 ? sortedReadings[0].value.toLocaleString('ru-RU', { maximumFractionDigits: 2 }) : '-')} {unit}

Посл. поверка

{meter.lastVerification || '-'}

След. поверка

{meter.nextVerification || '-'}

{meter.manufacturer && (
Производитель

{meter.manufacturer}

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

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

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

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

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

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

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

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

Минимальное

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

Период

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

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

)} {/* Graph */} {sortedReadings.length > 0 && (() => { const readingsWithConsumption = sortedReadings.filter(r => r.consumption !== undefined && r.consumption > 0); const maxConsumption = Math.max(...readingsWithConsumption.map(r => r.consumption || 0), 1); return readingsWithConsumption.length > 0 ? (

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

{readingsWithConsumption.slice(0, 12).reverse().map((r, i) => (
{new Date(r.date).toLocaleDateString('ru-RU')}: {(r.consumption || 0).toLocaleString('ru-RU')} {unit}
))}
Начало Сейчас
) : null; })()} {/* History List */}

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

{onAddReading && ( )}
{showAddModal && onAddReading && ( setShowAddModal(false)} onAdd={handleAddReading} /> )}
{sortedReadings.length > 0 ? sortedReadings.map((r, i) => ( )) : ( )}
Дата Показание Расход Источник
{new Date(r.date).toLocaleDateString('ru-RU')} {r.value.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit} {r.consumption !== undefined && r.consumption > 0 && ( <> {r.consumption.toLocaleString('ru-RU', { maximumFractionDigits: 2 })} {unit} )} {r.source === 'app' ? 'Прил.' : r.source === 'manual' ? 'Офис' : 'IoT'}
Нет данных о показаниях. Добавьте первое показание.
); }; // Sub-component: Resident Profile Modal const ResidentProfileModal: React.FC<{ profile: ResidentProfile | undefined; personName: string; personPhone?: string; onClose: () => void; onSave: (profile: ResidentProfile) => void; }> = ({ profile, personName, personPhone, onClose, onSave }) => { const [formData, setFormData] = useState(profile || { sentiment: 'neutral', notes: '', }); const sentimentConfig = { negative: { label: 'Негативен', icon: Frown, color: 'text-red-600 bg-red-50 border-red-200' }, toxic: { label: 'Токсичен', icon: AlertTriangle, color: 'text-orange-600 bg-orange-50 border-orange-200' }, positive: { label: 'Позитивен', icon: Smile, color: 'text-emerald-600 bg-emerald-50 border-emerald-200' }, loyal: { label: 'Лоялен', icon: Heart, color: 'text-blue-600 bg-blue-50 border-blue-200' }, neutral: { label: 'Нейтрален', icon: User, color: 'text-slate-600 bg-slate-50 border-slate-200' }, }; const handleSave = () => { onSave(formData); onClose(); }; return (
e.stopPropagation()}>

{personName}

{personPhone && (

{personPhone}

)}
{(Object.keys(sentimentConfig) as Array).map(key => { const config = sentimentConfig[key]; const Icon = config.icon; return ( ); })}
setFormData({ ...formData, birthday: e.target.value })} className="w-full p-3 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none" />
setFormData({ ...formData, email: e.target.value })} placeholder="email@example.com" className="w-full p-3 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none" />