Files
mkd/components/development/auditInspectionSchema.ts

149 lines
6.4 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
/**
* Справочник пунктов и подпунктов осмотра аудита.
* Пункты: Кровля, Фасад, Подъезды, Инфраструктура, Придомовой участок.
*/
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: 'Площадка для мусора' },
],
},
];
/** Оценки 15 для подпунктов */
export const INSPECTION_SCORE_LABELS: Record<number, string> = {
1: '1 — очень плохо',
2: '2 — плохо',
3: '3 — удовлетворительно',
4: '4 — хорошо',
5: '5 — отлично',
};
export const AUDIT_STATUS_LABELS: Record<string, string> = {
new: 'Новый',
in_progress: 'В работе',
completed: 'Завершён',
};
/** Нормализация оценки для отображения: число 15 или старые 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;
}
/** Среднее по подпунктам категории: только оценки 15, без 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 — числа 15 или 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);
}
/** Индекс сложности (0100) из средних по пунктам осмотра — та же формула, что и износ: (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;
}
/** Текст итога по среднему 15 (для отображения) */
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} — очень плохо`;
}