Files
mkd/components/development/auditInspectionSchema.ts
2026-02-04 00:17:04 +05:00

149 lines
6.4 KiB
TypeScript
Executable File
Raw Permalink 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 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} — очень плохо`;
}