import React, { useState, useMemo, useEffect } from 'react'; import { Building, MeterCheckRound, PersonalAccount, AccountMeter, MeterReading } from '../../types'; import { ClipboardCheck, Plus, ChevronRight, Search, DoorClosed, CheckCircle2, RotateCcw, X, ArrowLeft, Gauge, AlertTriangle, Activity, User, ShieldAlert, TrendingUp, Zap, Droplets, Flame } from 'lucide-react'; import { storageService } from '../../services/storageService'; import { backendApi } from '../../services/apiClient'; // ==================================================================================== // UTILS // ==================================================================================== const getMeterIcon = (type: string) => { const t = type.toLowerCase(); if (t.includes('гвс') || t.includes('хвс') || t.includes('вод')) return Droplets; if (t.includes('эл') || t.includes('э/э')) return Zap; if (t.includes('газ')) return Flame; return Gauge; }; /** При завершении обхода записывает показания в ОДПУ (passport.meters) и квартирные ПУ (accounts[].meters). */ function applyRoundReadingsToBuilding(building: Building, round: MeterCheckRound): Building { const today = new Date().toISOString().slice(0, 10); const b: Building = { ...building }; b.passport = { ...b.passport, meters: [...(b.passport?.meters || [])] }; b.accounts = [...(b.accounts || [])]; const addReading = (prevVal: number, curVal: number): MeterReading => ({ date: today, value: curVal, consumption: Math.max(0, curVal - prevVal), source: 'manual' }); round.odpuMeters?.forEach((om) => { if (om.status !== 'verified' || om.currentValue == null) return; const idx = om.meterIndex; const m = b.passport!.meters![idx]; if (!m) return; const reading = addReading(om.previousValue ?? 0, om.currentValue); const readings = [...(m.readings || []), reading].sort((a, b) => b.date.localeCompare(a.date)); b.passport!.meters![idx] = { ...m, readings, currentReading: om.currentValue }; }); const accMap = new Map(); b.accounts.forEach((a) => accMap.set(a.apartmentNumber, a)); round.apartments?.forEach((apt) => { if (apt.status !== 'verified') return; const acc = accMap.get(apt.apartmentNumber); if (!acc || !acc.meters?.length) return; let newMeters = [...acc.meters]; apt.readings.forEach((r) => { if (r.currentValue == null) return; const mi = newMeters.findIndex((m) => m.id === r.meterId); if (mi < 0) return; const meter = newMeters[mi]; const reading = addReading(r.previousValue ?? 0, r.currentValue); const meterReadings = [...(meter.readings || []), reading].sort((a, b) => b.date.localeCompare(a.date)); newMeters[mi] = { ...meter, readings: meterReadings, currentReading: r.currentValue }; }); accMap.set(apt.apartmentNumber, { ...acc, meters: newMeters }); }); b.accounts = b.accounts.map((a) => accMap.get(a.apartmentNumber) ?? a); const rounds = [...(b.meterCheckRounds || [])]; const ri = rounds.findIndex((r) => r.id === round.id); if (ri >= 0) rounds[ri] = round; else rounds.unshift(round); b.meterCheckRounds = rounds; return b; } /** Сохраняет здание в localStorage и в БД (backend). */ async function persistBuilding(updated: Building): Promise { storageService.saveBuildingData(updated); try { await backendApi.updateBuilding(updated); } catch (e) { console.warn('[MeterCheckView] Не удалось синхронизировать с БД:', e); } } // ==================================================================================== // ANALYTICS SUB-COMPONENT // ==================================================================================== const RoundAnalytics: React.FC<{ round: MeterCheckRound }> = ({ round }) => { const issues = useMemo(() => { const results = { suspicious: [] as any[], noReadings: [] as any[], bigGap: [] as any[] }; // Анализ квартир round.apartments?.forEach(apt => { if (apt.status === 'verified') { apt.readings.forEach(r => { const diff = (r.currentValue || 0) - r.previousValue; // Example logic: if user reading was suspiciously low or gap is huge if (diff > 50) results.bigGap.push({ apt: apt.apartmentNumber, type: r.meterType, val: diff }); if (r.currentValue === r.previousValue && r.previousValue !== 0) { results.suspicious.push({ apt: apt.apartmentNumber, type: r.meterType }); } }); } if (apt.status === 'no_access') { results.noReadings.push(apt.apartmentNumber); } }); // Анализ ОДПУ round.odpuMeters?.forEach(meter => { if (meter.status === 'verified') { const diff = (meter.currentValue || 0) - meter.previousValue; if (diff > 50) results.bigGap.push({ apt: meter.meterName, type: meter.meterType, val: diff }); if (meter.currentValue === meter.previousValue && meter.previousValue !== 0) { results.suspicious.push({ apt: meter.meterName, type: meter.meterType }); } } if (meter.status === 'no_access') { results.noReadings.push(meter.meterName); } }); return results; }, [round]); return (

Аномальный расход

{issues.bigGap.length}

Разница {'>'} 50 ед. от прошлого периода

Подозрение на магнит

{issues.suspicious.length}

Нулевое потребление при наличии жильцов

Недопуск

{issues.noReadings.length}

Требуется повторный вызов или уведомление

); }; // ==================================================================================== // ROUND DETAILS // ==================================================================================== const RoundDetails: React.FC<{ round: MeterCheckRound, onSave: (round: MeterCheckRound) => void, onBack: () => void, building: Building }> = ({ round, onSave, onBack, building }) => { const [data, setData] = useState(round); const [selectedApt, setSelectedApt] = useState(null); const [selectedOdpuMeter, setSelectedOdpuMeter] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [viewMode, setViewMode] = useState<'list' | 'stats'>('list'); /** Локальные значения показаний в модалке квартиры (meterId -> currentValue) для корректного сохранения. */ const [aptModalValues, setAptModalValues] = useState>({}); const currentApt = data.apartments?.find(a => a.apartmentNumber === selectedApt); const currentOdpuMeter = data.odpuMeters?.find(m => m.meterId === selectedOdpuMeter); useEffect(() => { if (!selectedApt || !currentApt) { setAptModalValues({}); return; } const v: Record = {}; currentApt.readings.forEach(r => { if (r.currentValue != null) v[r.meterId] = r.currentValue; }); setAptModalValues(v); }, [selectedApt, currentApt?.apartmentNumber]); const handleUpdateApt = (aptNumber: string, status: 'verified' | 'no_access', readings?: any) => { setData(prev => ({ ...prev, apartments: prev.apartments?.map(a => a.apartmentNumber === aptNumber ? { ...a, status, checkedAt: new Date().toISOString(), readings: readings || a.readings } : a ) || [] })); setSelectedApt(null); }; const handleUpdateOdpuMeter = (meterId: string, status: 'verified' | 'no_access', currentValue?: number) => { setData(prev => ({ ...prev, odpuMeters: prev.odpuMeters?.map(m => m.meterId === meterId ? { ...m, status, checkedAt: new Date().toISOString(), currentValue: currentValue || m.currentValue } : m ) || [] })); setSelectedOdpuMeter(null); }; const stats = { total: (data.apartments?.length || 0) + (data.odpuMeters?.length || 0), verified: (data.apartments?.filter(a => a.status === 'verified').length || 0) + (data.odpuMeters?.filter(m => m.status === 'verified').length || 0), noAccess: (data.apartments?.filter(a => a.status === 'no_access').length || 0) + (data.odpuMeters?.filter(m => m.status === 'no_access').length || 0), apartments: { total: data.apartments?.length || 0, verified: data.apartments?.filter(a => a.status === 'verified').length || 0, noAccess: data.apartments?.filter(a => a.status === 'no_access').length || 0, }, odpu: { total: data.odpuMeters?.length || 0, verified: data.odpuMeters?.filter(m => m.status === 'verified').length || 0, noAccess: data.odpuMeters?.filter(m => m.status === 'no_access').length || 0, } }; const filteredApts = data.apartments?.filter(a => a.apartmentNumber.includes(searchTerm) ) || []; const filteredOdpuMeters = data.odpuMeters?.filter(m => m.meterName.toLowerCase().includes(searchTerm.toLowerCase()) || m.meterType.toLowerCase().includes(searchTerm.toLowerCase()) ) || []; return (

Обход {data.date}

{data.roundType === 'odpu' ? 'Только ОДПУ' : data.roundType === 'apartments' ? 'По квартирно' : 'Все счетчики'}

{(data.selectedEntrances && data.selectedEntrances.length > 0) && (

Подъезды: {data.selectedEntrances.sort((a, b) => a - b).join(', ')}

)} {(data.selectedFloors && data.selectedFloors.length > 0) && (

Этажи: {data.selectedFloors.sort((a, b) => a - b).join(', ')}

)}
{viewMode === 'stats' ? (

Журнал выявленных расхождений

{/* Расхождения по квартирам */} {data.apartments?.filter(a => a.status === 'verified').map(apt => ( apt.readings.map(r => { const diff = (r.currentValue || 0) - r.previousValue; if (diff > 40) { return (
Кв.{apt.apartmentNumber}

{r.meterType}

Расход: +{diff} ед.

); } return null; }) ))} {/* Расхождения по ОДПУ */} {data.odpuMeters?.filter(m => m.status === 'verified').map(meter => { const diff = (meter.currentValue || 0) - meter.previousValue; if (diff > 40) { return (

{meter.meterName}

Расход: +{diff.toLocaleString('ru-RU')} ед.

); } return null; })} {((data.apartments?.filter(a => a.status === 'verified').length || 0) === 0 && (data.odpuMeters?.filter(m => m.status === 'verified').length || 0) === 0) && (

Нет выявленных расхождений

)}
) : ( <>
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-3 rounded-2xl border border-slate-200 bg-white focus:ring-2 focus:ring-primary-500 outline-none shadow-sm" />
{stats.verified} / {stats.total} Готово
{/* ОДПУ секция */} {(data.roundType === 'odpu' || data.roundType === 'all') && data.odpuMeters && data.odpuMeters.length > 0 && (

Общедомовые ПУ (ОДПУ)

{filteredOdpuMeters.map(meter => (
setSelectedOdpuMeter(meter.meterId)} className={`bg-white p-4 rounded-3xl border transition-all cursor-pointer relative group ${ meter.status === 'verified' ? 'border-emerald-200 hover:border-emerald-300' : meter.status === 'no_access' ? 'border-red-200 hover:border-red-300' : 'border-slate-100 hover:border-primary-300 shadow-sm' }`} >

{meter.meterName}

{meter.status === 'verified' ? (
) : meter.status === 'no_access' ? (
) : null}

Предыдущее: {meter.previousValue.toLocaleString('ru-RU')}

{meter.currentValue !== undefined && (

Текущее: {meter.currentValue.toLocaleString('ru-RU')}

)}
))}
)} {/* Квартиры секция */} {(data.roundType === 'apartments' || data.roundType === 'all') && data.apartments && data.apartments.length > 0 && (

Индивидуальные ПУ (по квартирам)

{/* Группировка по подъездам и этажам, если есть фильтры */} {(() => { // Если выбраны подъезды, группируем по подъездам if (data.selectedEntrances && data.selectedEntrances.length > 0) { return data.selectedEntrances.sort((a, b) => a - b).map(entranceNum => { let entranceApts = filteredApts.filter(apt => apt.entranceNumber === entranceNum); // Если также выбраны этажи, дополнительно фильтруем if (data.selectedFloors && data.selectedFloors.length > 0) { entranceApts = entranceApts.filter(apt => apt.floor && data.selectedFloors!.includes(apt.floor)); } if (entranceApts.length === 0) return null; // Группируем по этажам внутри подъезда, если выбраны этажи if (data.selectedFloors && data.selectedFloors.length > 0) { return (

Подъезд {entranceNum}

{data.selectedFloors.sort((a, b) => a - b).map(floorNum => { const floorApts = entranceApts.filter(apt => apt.floor === floorNum); if (floorApts.length === 0) return null; return (
Этаж {floorNum}
{floorApts.map(apt => (
setSelectedApt(apt.apartmentNumber)} className={`bg-white p-4 rounded-3xl border transition-all cursor-pointer relative group ${ apt.status === 'verified' ? 'border-emerald-200 hover:border-emerald-300' : apt.status === 'no_access' ? 'border-red-200 hover:border-red-300' : 'border-slate-100 hover:border-primary-300 shadow-sm' }`} >

Кв. {apt.apartmentNumber}

{apt.status === 'verified' ? (
) : apt.status === 'no_access' ? (
) : null}
{apt.readings.map(r => { const Icon = getMeterIcon(r.meterType); return
})}
))}
); })}
); } // Только подъезды без группировки по этажам return (

Подъезд {entranceNum}

{entranceApts.map(apt => (
setSelectedApt(apt.apartmentNumber)} className={`bg-white p-4 rounded-3xl border transition-all cursor-pointer relative group ${ apt.status === 'verified' ? 'border-emerald-200 hover:border-emerald-300' : apt.status === 'no_access' ? 'border-red-200 hover:border-red-300' : 'border-slate-100 hover:border-primary-300 shadow-sm' }`} >

Кв. {apt.apartmentNumber}

{apt.status === 'verified' ? (
) : apt.status === 'no_access' ? (
) : null}
{apt.readings.map(r => { const Icon = getMeterIcon(r.meterType); return
})}
))}
); }); } // Если выбраны только этажи (без подъездов) if (data.selectedFloors && data.selectedFloors.length > 0) { return data.selectedFloors.sort((a, b) => a - b).map(floorNum => { const floorApts = filteredApts.filter(apt => apt.floor === floorNum); if (floorApts.length === 0) return null; return (

Этаж {floorNum}

{floorApts.map(apt => (
setSelectedApt(apt.apartmentNumber)} className={`bg-white p-4 rounded-3xl border transition-all cursor-pointer relative group ${ apt.status === 'verified' ? 'border-emerald-200 hover:border-emerald-300' : apt.status === 'no_access' ? 'border-red-200 hover:border-red-300' : 'border-slate-100 hover:border-primary-300 shadow-sm' }`} >

Кв. {apt.apartmentNumber}

{apt.status === 'verified' ? (
) : apt.status === 'no_access' ? (
) : null}
{apt.readings.map(r => { const Icon = getMeterIcon(r.meterType); return
})}
))}
); }); } // Нет фильтров - показываем все квартиры return (
{filteredApts.map(apt => (
setSelectedApt(apt.apartmentNumber)} className={`bg-white p-4 rounded-3xl border transition-all cursor-pointer relative group ${ apt.status === 'verified' ? 'border-emerald-200 hover:border-emerald-300' : apt.status === 'no_access' ? 'border-red-200 hover:border-red-300' : 'border-slate-100 hover:border-primary-300 shadow-sm' }`} >

Кв. {apt.apartmentNumber}

{apt.status === 'verified' ? (
) : apt.status === 'no_access' ? (
) : null}
{apt.readings.map(r => { const Icon = getMeterIcon(r.meterType); return
})}
))}
); })()}
)} )} {/* ODPU Meter Verification Modal */} {currentOdpuMeter && (() => { const meters = building.passport?.meters || []; const actualMeter = meters[currentOdpuMeter.meterIndex]; const Icon = getMeterIcon(currentOdpuMeter.meterType); const currentInput = currentOdpuMeter.currentValue || 0; const diff = currentInput ? currentInput - currentOdpuMeter.previousValue : 0; const isWarning = diff > 30 || diff < 0; const unit = actualMeter?.unit || (currentOdpuMeter.meterType === 'Electricity' ? 'кВт⋅ч' : 'м³'); return (
setSelectedOdpuMeter(null)}>
e.stopPropagation()}>

{currentOdpuMeter.meterName}

ОДПУ - Акт сверки показаний

Предыдущее: {currentOdpuMeter.previousValue} {isWarning && diff > 0 && }
{ const val = Number(e.target.value); const updated = data.odpuMeters?.map(m => m.meterId === currentOdpuMeter.meterId ? { ...m, currentValue: val } : m ) || []; setData(prev => ({ ...prev, odpuMeters: updated })); }} className={`w-full p-4 bg-white border-2 rounded-2xl text-2xl font-black text-slate-900 outline-none transition-all ${isWarning ? 'border-red-200 focus:border-red-500' : 'border-slate-100 focus:border-primary-500'}`} /> {diff > 0 && (
Потребление за период: +{diff.toLocaleString('ru-RU')} {unit} {isWarning ? : }
)}
); })()} {/* Apartment Verification Modal */} {currentApt && (
setSelectedApt(null)}>
e.stopPropagation()}>

Квартира {currentApt.apartmentNumber}

Акт сверки показаний

{currentApt.readings.map((meter) => { const Icon = getMeterIcon(meter.meterType); const currentInput = aptModalValues[meter.meterId] ?? meter.currentValue ?? 0; const numInput = typeof currentInput === 'number' && !Number.isNaN(currentInput) ? currentInput : 0; const diff = numInput > 0 ? numInput - meter.previousValue : 0; const isWarning = diff > 30 || diff < 0; return (
Предыдущее: {meter.previousValue} {isWarning && diff > 0 && }
{ const val = e.target.value === '' ? 0 : Number(e.target.value); setAptModalValues(prev => ({ ...prev, [meter.meterId]: val })); }} className={`w-full p-4 bg-white border-2 rounded-2xl text-2xl font-black text-slate-900 outline-none transition-all ${isWarning ? 'border-red-200 focus:border-red-500' : 'border-slate-100 focus:border-primary-500'}`} /> {diff > 0 && (
Потребление за период: +{diff} ед. {isWarning ? : }
)}
); })}
)}
); }; // Модальное окно выбора типа обхода с фильтрами const RoundTypeModal: React.FC<{ isOpen: boolean; onClose: () => void; onSelect: (type: 'odpu' | 'apartments' | 'all', filters?: { entrances?: number[], floors?: number[] }) => void; building: Building; }> = ({ isOpen, onClose, onSelect, building }) => { const [selectedType, setSelectedType] = useState<'odpu' | 'apartments' | 'all' | null>(null); const [selectedEntrances, setSelectedEntrances] = useState([]); const [selectedFloors, setSelectedFloors] = useState([]); const entrancesCount = building.passport?.general?.entrancesCount || building.entrances?.length || 0; const floorsCount = building.passport?.general?.floors || 0; const toggleEntrance = (num: number) => { setSelectedEntrances(prev => prev.includes(num) ? prev.filter(n => n !== num) : [...prev, num] ); }; const toggleFloor = (num: number) => { setSelectedFloors(prev => prev.includes(num) ? prev.filter(n => n !== num) : [...prev, num] ); }; const handleConfirm = () => { if (!selectedType) return; const filters: { entrances?: number[], floors?: number[] } = {}; if (selectedEntrances.length > 0) filters.entrances = selectedEntrances; if (selectedFloors.length > 0) filters.floors = selectedFloors; onSelect(selectedType, Object.keys(filters).length > 0 ? filters : undefined); onClose(); // Сброс setSelectedType(null); setSelectedEntrances([]); setSelectedFloors([]); }; if (!isOpen) return null; return (
e.stopPropagation()}>

Выберите тип обхода

{/* Выбор типа обхода */}
{/* Фильтры (только для квартир) */} {selectedType === 'apartments' || selectedType === 'all' ? ( <> {entrancesCount > 0 && (
{Array.from({ length: entrancesCount }, (_, i) => i + 1).map(num => ( ))}
)} {floorsCount > 0 && (
{Array.from({ length: floorsCount }, (_, i) => i + 1).map(num => ( ))}
)} ) : null} {/* Кнопка подтверждения */}
); }; export const MeterCheckView: React.FC<{ building: Building, setBuilding: React.Dispatch> }> = ({ building, setBuilding }) => { const [activeRoundId, setActiveRoundId] = useState(null); const [showRoundTypeModal, setShowRoundTypeModal] = useState(false); const handleCreateRound = (roundType: 'odpu' | 'apartments' | 'all', filters?: { entrances?: number[], floors?: number[] }) => { const meters = building.passport?.meters || []; const odpuMeters = meters.filter(m => m.hasMeter).map((meter, idx) => { const lastReading = meter.readings && meter.readings.length > 0 ? meter.readings[0].value : (meter.currentReading || 0); return { meterId: `odpu-${idx}`, meterIndex: meters.findIndex(m => m === meter), meterType: meter.resource || 'Other', meterName: meter.name || meter.resource || 'ПУ', previousValue: lastReading, status: 'pending' as const, }; }); // Фильтруем квартиры по подъездам и этажам let filteredAccounts = building.accounts; // Функция для определения подъезда квартиры const getEntranceForApartment = (acc: any): number | undefined => { // Сначала проверяем, есть ли явное поле entranceNumber if (acc.entranceNumber) return acc.entranceNumber; // Пытаемся найти в building.entrances const entrance = building.entrances?.find(e => e.sections?.some(s => s.elements.some(el => el.name.includes(acc.apartmentNumber))) ); if (entrance) return entrance.number; // Если есть несколько подъездов, пытаемся определить по номеру квартиры // (обычно квартиры распределяются по подъездам примерно равномерно) const entrancesCount = building.passport?.general?.entrancesCount || building.entrances?.length || 0; if (entrancesCount > 0) { // Простая эвристика: делим квартиры по подъездам const aptNum = parseInt(acc.apartmentNumber) || 0; if (aptNum > 0) { // Предполагаем, что квартиры распределены примерно равномерно // Например, если 3 подъезда и квартиры 1-90, то подъезд 1 = 1-30, подъезд 2 = 31-60, подъезд 3 = 61-90 const totalApts = building.accounts.length; const aptsPerEntrance = Math.ceil(totalApts / entrancesCount); const calculatedEntrance = Math.ceil(aptNum / aptsPerEntrance); if (calculatedEntrance >= 1 && calculatedEntrance <= entrancesCount) { return calculatedEntrance; } } } return undefined; }; if (filters?.entrances && filters.entrances.length > 0) { filteredAccounts = filteredAccounts.filter(acc => { const entranceNum = getEntranceForApartment(acc); return entranceNum && filters.entrances!.includes(entranceNum); }); } if (filters?.floors && filters.floors.length > 0) { filteredAccounts = filteredAccounts.filter(acc => acc.floor && filters.floors!.includes(acc.floor) ); } const apartments = filteredAccounts.map(acc => ({ apartmentNumber: acc.apartmentNumber, entranceNumber: getEntranceForApartment(acc), floor: acc.floor || undefined, status: 'pending' as const, readings: acc.meters.map(m => ({ meterId: m.id, meterType: m.type, previousValue: m.readings[0]?.value || 0 })) })); const newRound: MeterCheckRound = { id: `round-${Date.now()}`, date: new Date().toLocaleDateString('ru-RU'), inspector: 'Алексей Петров', status: 'active', buildingId: building.id, roundType, selectedEntrances: filters?.entrances, selectedFloors: filters?.floors, ...(roundType === 'odpu' || roundType === 'all' ? { odpuMeters } : {}), ...(roundType === 'apartments' || roundType === 'all' ? { apartments } : {}), }; const next: Building = { ...building, meterCheckRounds: [newRound, ...(building.meterCheckRounds || [])] }; setBuilding(next); persistBuilding(next); setActiveRoundId(newRound.id); }; const handleUpdateRound = (updatedRound: MeterCheckRound) => { let next: Building; if (updatedRound.status === 'completed') { next = applyRoundReadingsToBuilding(building, updatedRound); } else { next = { ...building, meterCheckRounds: (building.meterCheckRounds || []).map(r => r.id === updatedRound.id ? updatedRound : r ) }; } setBuilding(next); persistBuilding(next); setActiveRoundId(null); }; const activeRound = building.meterCheckRounds?.find(r => r.id === activeRoundId); if (activeRound) { return setActiveRoundId(null)} building={building} />; } return (

Обходы и Контроль ПУ

Инструмент для выявления занижений показаний и контроля состояния индивидуальных приборов учета.

setShowRoundTypeModal(false)} onSelect={handleCreateRound} building={building} />

Архив обходов и сверок

{(!building.meterCheckRounds || building.meterCheckRounds.length === 0) && (

Нет записей

)}
{building.meterCheckRounds?.map(round => { const totalCount = (round.apartments?.length || 0) + (round.odpuMeters?.length || 0); const verifiedCount = (round.apartments?.filter(a => a.status === 'verified').length || 0) + (round.odpuMeters?.filter(m => m.status === 'verified').length || 0); const progress = totalCount > 0 ? (verifiedCount / totalCount) * 100 : 0; const roundTypeLabel = round.roundType === 'odpu' ? 'ОДПУ' : round.roundType === 'apartments' ? 'Квартиры' : 'Все'; return (
setActiveRoundId(round.id)} className="bg-white p-5 rounded-3xl border border-slate-200 shadow-sm hover:shadow-lg hover:border-primary-200 transition-all cursor-pointer group" >
{round.status === 'completed' ? 'Завершен' : 'В процессе'}

Сверка от {round.date}

{round.inspector}
{roundTypeLabel} {round.selectedEntrances && round.selectedEntrances.length > 0 && ( Подъезды: {round.selectedEntrances.sort((a, b) => a - b).join(', ')} )} {round.selectedFloors && round.selectedFloors.length > 0 && ( Этажи: {round.selectedFloors.sort((a, b) => a - b).join(', ')} )}
Прогресс {verifiedCount} / {totalCount}
); })}
); };