/** * Справочник пунктов и подпунктов осмотра аудита. * Пункты: Кровля, Фасад, Подъезды, Инфраструктура, Придомовой участок. */ import type { InspectionCategoryKey } from '../../types'; export interface InspectionSubItemSchema { key: string; label: string; } export interface InspectionCategorySchema { key: InspectionCategoryKey; label: string; subItems: InspectionSubItemSchema[]; } export const AUDIT_INSPECTION_SCHEMA: InspectionCategorySchema[] = [ { key: 'roof', label: 'Кровля', subItems: [ { key: 'covering', label: 'Покрытие кровли' }, { key: 'waterproofing', label: 'Гидроизоляция' }, { key: 'drainage', label: 'Водосточная система' }, { key: 'fencing', label: 'Ограждение кровли' }, { key: 'hatches', label: 'Люки и выходы' }, ], }, { key: 'facade', label: 'Фасад', subItems: [ { key: 'walls', label: 'Состояние стен' }, { key: 'joints', label: 'Межпанельные швы' }, { key: 'windows', label: 'Оконные блоки' }, { key: 'balconies', label: 'Балконы и лоджии' }, { key: 'basement_floor', label: 'Цокольный этаж' }, ], }, { key: 'entrances', label: 'Подъезды', subItems: [ { key: 'doors', label: 'Входные двери' }, { key: 'stairs', label: 'Лестничные марши' }, { key: 'railings', label: 'Перила и ограждения' }, { key: 'lighting', label: 'Освещение' }, { key: 'walls_ceilings', label: 'Стены и потолки' }, { key: 'elevator', label: 'Лифт (при наличии)' }, ], }, { key: 'infrastructure', label: 'Инфраструктура', subItems: [ { key: 'heating', label: 'Система отопления' }, { key: 'water', label: 'ХВС/ГВС' }, { key: 'sewage', label: 'Канализация' }, { key: 'electrical', label: 'Электрощитовые' }, { key: 'basement', label: 'Подвал / ИТП' }, { key: 'gas', label: 'Газовое оборудование (при наличии)' }, ], }, { key: 'yard', label: 'Придомовой участок', subItems: [ { key: 'paving', label: 'Благоустройство территории' }, { key: 'lighting', label: 'Наружное освещение' }, { key: 'parking', label: 'Парковка' }, { key: 'playground', label: 'Детская площадка (при наличии)' }, { key: 'waste', label: 'Площадка для мусора' }, ], }, ]; /** Оценки 1–5 для подпунктов */ export const INSPECTION_SCORE_LABELS: Record = { 1: '1 — очень плохо', 2: '2 — плохо', 3: '3 — удовлетворительно', 4: '4 — хорошо', 5: '5 — отлично', }; export const AUDIT_STATUS_LABELS: Record = { new: 'Новый', in_progress: 'В работе', completed: 'Завершён', }; /** Нормализация оценки для отображения: число 1–5 или старые good/fair/poor → 5/3/1. */ export function normalizeRatingToScore(rating: unknown): number | null { if (rating != null && typeof rating === 'number' && rating >= 1 && rating <= 5) return rating; if (rating === 'good') return 5; if (rating === 'fair') return 3; if (rating === 'poor') return 1; return null; } /** Среднее по подпунктам категории: только оценки 1–5, без noAccess/notPresent. Округление до 1 знака. */ export function calcCategoryAverage(subItems: { rating: number | null; noAccess?: boolean; notPresent?: boolean }[]): number | null { const valid = subItems.filter(s => { const score = normalizeRatingToScore(s.rating); return score != null && !s.noAccess && !s.notPresent; }); if (valid.length === 0) return null; const sum = valid.reduce((a, s) => a + (normalizeRatingToScore(s.rating) ?? 0), 0); return Math.round((sum / valid.length) * 10) / 10; } /** Износ % из средних по пунктам: (5 − среднее) / 4 × 100. categoryAverages — числа 1–5 или null. */ export function calcWearPercentFromCategoryAverages(categoryAverages: (number | null)[]): number | null { const valid = categoryAverages.filter((a): a is number => a != null && a >= 1 && a <= 5); if (valid.length === 0) return null; const avg = valid.reduce((s, a) => s + a, 0) / valid.length; return Math.round(((5 - avg) / 4) * 100); } /** Индекс сложности (0–100) из средних по пунктам осмотра — та же формула, что и износ: (5 − среднее) / 4 × 100. */ export function calcComplexityIndexFromCategoryAverages(categoryAverages: (number | null)[]): number | null { return calcWearPercentFromCategoryAverages(categoryAverages); } /** Прогнозный тариф по износу %, индексу сложности и марже %. Формула как на бэкенде. */ export function calculateTariffFromAudit(params: { wearPercent: number; complexityIndex: number; projectedMargin: number }): number { const BASE_TARIFF = 28; const wearPercent = Math.max(0, Math.min(100, Number(params.wearPercent) || 0)); const complexityIndex = Math.max(0, Math.min(100, Number(params.complexityIndex) ?? 50)); const marginPercent = Math.max(0, Math.min(50, Number(params.projectedMargin) || 15)); const wearFactor = 1 + (wearPercent / 100) * 0.5; const complexityFactor = 1 + (complexityIndex / 100) * 0.2; const marginFactor = 1 / (1 - marginPercent / 100); const tariff = BASE_TARIFF * wearFactor * complexityFactor * marginFactor; return Math.round(tariff * 100) / 100; } /** Текст итога по среднему 1–5 (для отображения) */ export function formatCategoryOverall(avg: number | null): string { if (avg == null) return '—'; if (avg >= 4.5) return `${avg} — отлично`; if (avg >= 3.5) return `${avg} — хорошо`; if (avg >= 2.5) return `${avg} — удовлетворительно`; if (avg >= 1.5) return `${avg} — плохо`; return `${avg} — очень плохо`; }