Files
site_aegisone/service/inc/functions.php
T
2026-05-17 05:22:06 +03:00

679 lines
32 KiB
PHP
Raw 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.
<?php
/**
* ============================================
* AegisOne Engineering — Расчётные формулы
* ============================================
*
* Все коэффициенты вынесены в БД (таблица formula_coefficients)
* и редактируются владельцем через /service/pages/owner/coefficients.php.
*
* Хардкоды ЗАПРЕЩЕНЫ — используй get_coefficient('key').
*
* Формат ключа: <формула>.<параметр>, например:
* risk_score.no_archive → 25
* object_index.risk_weight → 0.4
* sla_price.base_cost → 15000
*/
// ========================================================================
// ВСПОМОГАТЕЛЬНЫЕ
// ========================================================================
/**
* Получить значение коэффициента из БД.
* Результат кэшируется в статической переменной (один SQL-запрос за вызов).
*
* @param string $key Ключ коэффициента (напр. 'risk_score.no_archive')
* @param float $default Значение по умолчанию, если ключ не найден
* @return float
*/
function get_coefficient(string $key, float $default = 0): float {
static $cache = null;
if ($cache === null) {
try {
$pdo = getDB();
$stmt = $pdo->query("SELECT `key`, `value` FROM formula_coefficients WHERE is_active=1");
$cache = [];
while ($row = $stmt->fetch()) {
$cache[$row['key']] = (float)$row['value'];
}
} catch (\Throwable $e) {
$cache = [];
}
}
return $cache[$key] ?? $default;
}
/**
* Форматирование: 1 234 567 ₽
*/
function fmt_money(float $amount): string {
return number_format($amount, 0, '.', ' ') . ' ₽';
}
/**
* Форматирование: 95.3%
*/
function fmt_percent(float $value): string {
return number_format($value, 1, '.', '') . '%';
}
/**
* Форматирование: 3.5 ч
*/
function fmt_hours(float $hours): string {
return number_format($hours, 1, '.', '') . ' ч';
}
/**
* CSS-класс для статуса / приоритета
*/
function badge_class(string $status, string $type = 'default'): string {
$map = [
'P1' => 'badge-critical', 'P2' => 'badge-high', 'P3' => 'badge-medium', 'P4' => 'badge-low',
'open' => 'badge-open', 'in_progress' => 'badge-progress', 'completed' => 'badge-done', 'cancelled' => 'badge-cancelled',
'active' => 'badge-active', 'inactive' => 'badge-inactive', 'draft' => 'badge-draft', 'final' => 'badge-done',
'Risk' => 'badge-risk', 'Crisis' => 'badge-crisis', 'Stable' => 'badge-stable', 'Growth' => 'badge-growth',
];
return $map[$status] ?? 'badge-default';
}
// ========================================================================
// 1. RISK SCORE (ОЦЕНКА РИСКА ОБЪЕКТА)
// ========================================================================
// Формула: сумма весов нарушений (каждое — 0 или 1).
// Веса хранятся в БД:
// risk_score.no_archive — нет архива видеонаблюдения (умолч. 25)
// risk_score.no_power_backup — нет резервного питания (умолч. 20)
// risk_score.no_regulations — нет регламента обслуживания (умолч. 15)
// risk_score.system_failures — частые сбои систем (умолч. 20)
// risk_score.no_documentation — нет документации (умолч. 10)
function calc_risk_score(bool $noArchive, bool $noPowerBackup, bool $noRegulations, bool $systemFailures, bool $noDocumentation): float {
return ($noArchive ? get_coefficient('risk_score.no_archive', 25) : 0)
+ ($noPowerBackup ? get_coefficient('risk_score.no_power_backup', 20) : 0)
+ ($noRegulations ? get_coefficient('risk_score.no_regulations', 15) : 0)
+ ($systemFailures ? get_coefficient('risk_score.system_failures', 20) : 0)
+ ($noDocumentation ? get_coefficient('risk_score.no_documentation', 10) : 0);
}
/**
* Интерпретация Risk Score
* risk_score.bound_low — граница низкого риска (умолч. 20)
* risk_score.bound_medium — граница среднего (умолч. 50)
* risk_score.bound_high — граница высокого (умолч. 75)
*/
function risk_label(float $score): string {
if ($score <= get_coefficient('risk_score.bound_low', 20)) return 'Низкий';
if ($score <= get_coefficient('risk_score.bound_medium', 50)) return 'Средний';
if ($score <= get_coefficient('risk_score.bound_high', 75)) return 'Высокий';
return 'Критический';
}
/**
* Множитель риска для расчёта SLA Price
* risk_multiplier.low — (0-20) умолч. 1.0
* risk_multiplier.medium — (21-50) умолч. 1.3
* risk_multiplier.high — (51-75) умолч. 1.6
* risk_multiplier.critical— (76-100) умолч. 2.0
*/
function risk_multiplier(float $score): float {
if ($score <= get_coefficient('risk_score.bound_low', 20)) return get_coefficient('risk_multiplier.low', 1.0);
if ($score <= get_coefficient('risk_score.bound_medium', 50)) return get_coefficient('risk_multiplier.medium', 1.3);
if ($score <= get_coefficient('risk_score.bound_high', 75)) return get_coefficient('risk_multiplier.high', 1.6);
return get_coefficient('risk_multiplier.critical', 2.0);
}
// ========================================================================
// 2. SLA COMPLEXITY INDEX (СЛОЖНОСТЬ ОБСЛУЖИВАНИЯ)
// ========================================================================
// Параметры:
// complexity.video_0_20 — до 20 камер (умолч. 10)
// complexity.video_20_100 — 20-100 камер (умолч. 20)
// complexity.video_100plus — 100+ камер (умолч. 35)
// complexity.access_0_5 — до 5 точек СКУД (умолч. 10)
// complexity.access_5_20 — 5-20 точек (умолч. 20)
// complexity.access_20plus — 20+ точек (умолч. 30)
// complexity.fire_simple — простая пожарка (умолч. 15)
// complexity.fire_medium — средняя (умолч. 25)
// complexity.fire_complex — сложная (>5000 м²) (умолч. 40)
// complexity.it — IT-инфраструктура (умолч. 10)
function calc_complexity(int $cameras, int $accessPoints, string $fireType, bool $hasIT): float {
$video = $cameras <= 20 ? get_coefficient('complexity.video_0_20', 10)
: ($cameras <= 100 ? get_coefficient('complexity.video_20_100', 20)
: get_coefficient('complexity.video_100plus', 35));
$access = $accessPoints <= 5 ? get_coefficient('complexity.access_0_5', 10)
: ($accessPoints <= 20 ? get_coefficient('complexity.access_5_20', 20)
: get_coefficient('complexity.access_20plus', 30));
$fire = $fireType === 'simple' ? get_coefficient('complexity.fire_simple', 15)
: ($fireType === 'medium' ? get_coefficient('complexity.fire_medium', 25)
: ($fireType === 'complex' ? get_coefficient('complexity.fire_complex', 40)
: 0));
$it = $hasIT ? get_coefficient('complexity.it', 10) : 0;
return $video + $access + $fire + $it;
}
function complexity_label(float $score): string {
if ($score <= 30) return 'Простой';
if ($score <= 70) return 'Средний';
if ($score <= 120) return 'Сложный';
return 'Enterprise';
}
// ========================================================================
// 3. INFRASTRUCTURE LOAD (НАГРУЗКА НА ИНФРАСТРУКТУРУ)
// ========================================================================
// infra.server_none — нет сервера (умолч. 15)
// infra.server_weak — слабый сервер (умолч. 10)
// infra.network_unstable — нестабильная сеть (умолч. 20)
// infra.network_partial — частично (умолч. 10)
// infra.power_none — нет UPS (умолч. 20)
// infra.power_weak — слабый UPS (умолч. 10)
function calc_infrastructure_load(string $serverState, string $networkState, string $powerState): float {
$server = $serverState === 'none' ? get_coefficient('infra.server_none', 15) : ($serverState === 'weak' ? get_coefficient('infra.server_weak', 10) : 0);
$network = $networkState === 'unstable' ? get_coefficient('infra.network_unstable', 20) : ($networkState === 'partial' ? get_coefficient('infra.network_partial', 10) : 0);
$power = $powerState === 'none' ? get_coefficient('infra.power_none', 20) : ($powerState === 'weak' ? get_coefficient('infra.power_weak', 10) : 0);
return $server + $network + $power;
}
// ========================================================================
// 4. SERVICE HISTORY INDEX (ИСТОРИЯ ОБСЛУЖИВАНИЯ)
// ========================================================================
// history.none — нет обслуживания (умолч. 30)
// history.irregular — нерегулярное (умолч. 20)
// history.formal — формальный подрядчик (умолч. 10)
// history.sla — есть SLA (умолч. 0)
function calc_service_history(string $serviceState): float {
if ($serviceState === 'none') return get_coefficient('history.none', 30);
if ($serviceState === 'irregular') return get_coefficient('history.irregular', 20);
if ($serviceState === 'formal') return get_coefficient('history.formal', 10);
if ($serviceState === 'sla') return get_coefficient('history.sla', 0);
return get_coefficient('history.none', 30);
}
// ========================================================================
// 5. OBJECT INDEX (ИТОГОВЫЙ ИНДЕКС ОБЪЕКТА)
// ========================================================================
// object_index.risk_weight — вес Risk Score (умолч. 0.4)
// object_index.complexity_weight — вес Complexity (умолч. 0.3)
// object_index.infra_weight — вес Infra Load (умолч. 0.2)
// object_index.history_weight — вес Service History (умолч. 0.1)
function calc_object_index(float $risk, float $complexity, float $infra, float $history): float {
return round(
$risk * get_coefficient('object_index.risk_weight', 0.4)
+ $complexity * get_coefficient('object_index.complexity_weight', 0.3)
+ $infra * get_coefficient('object_index.infra_weight', 0.2)
+ $history * get_coefficient('object_index.history_weight', 0.1)
, 2);
}
function object_class(float $index): string {
if ($index <= 30) return 'A';
if ($index <= 60) return 'B';
if ($index <= 90) return 'C';
return 'D';
}
function object_class_label(float $index): string {
$cls = object_class($index);
if ($cls === 'A') return 'Лёгкий SLA';
if ($cls === 'B') return 'Стандарт SLA';
if ($cls === 'C') return 'Сложный SLA';
if ($cls === 'D') return 'Enterprise / Высокий риск';
return '';
}
// ========================================================================
// 6. SLA PRICE (СТОИМОСТЬ SLA)
// ========================================================================
// sla_price.base_cost — базовая стоимость инженера (умолч. 15000)
// sla_price.region_* — региональные коэффициенты
// sla_price.risk_* — мультипликаторы риска (через risk_multiplier)
function calc_sla_price(float $baseCost, float $objectIndex, float $regionFactor, float $riskMult): float {
return round($baseCost * $objectIndex * $regionFactor * $riskMult, 2);
}
// ========================================================================
// 7. ENGINEER SCORE (ES) — ИТОГОВЫЙ ИНДЕКС ИНЖЕНЕРА
// ========================================================================
// engineer.score.sla_weight — 0.25
// engineer.score.response_weight — 0.20
// engineer.score.resolution_weight — 0.20
// engineer.score.diagnosis_weight — 0.15
// engineer.score.reopen_weight — 0.10
// engineer.score.risk_coverage_weight — 0.10
function calc_engineer_score(float $slaCompliance, float $responseTimeScore, float $resolutionTimeScore, float $diagnosisAccuracy, float $reopenRateScore, float $riskCoverageScore): float {
return round(
(get_coefficient('engineer.score.sla_weight', 0.25) * $slaCompliance)
+ (get_coefficient('engineer.score.response_weight', 0.20) * $responseTimeScore)
+ (get_coefficient('engineer.score.resolution_weight', 0.20) * $resolutionTimeScore)
+ (get_coefficient('engineer.score.diagnosis_weight', 0.15) * $diagnosisAccuracy)
+ (get_coefficient('engineer.score.reopen_weight', 0.10) * $reopenRateScore)
+ (get_coefficient('engineer.score.risk_coverage_weight', 0.10) * $riskCoverageScore)
, 2);
}
/**
* Грейд инженера по итоговому Score
* engineer.grade.senior — 90
* engineer.grade.strong — 80
* engineer.grade.middle — 70
*/
function engineer_grade(float $score): string {
if ($score >= get_coefficient('engineer.grade.senior', 90)) return 'Senior';
if ($score >= get_coefficient('engineer.grade.strong', 80)) return 'Strong';
if ($score >= get_coefficient('engineer.grade.middle', 70)) return 'Middle';
return 'Junior';
}
// ========================================================================
// 8. ENGINEER CONTROL SCORE (ECS) — ИНДЕКС УПРАВЛЕНИЯ ИНЖЕНЕРА
// ========================================================================
// ecs.sla_weight — 0.30
// ecs.task_dist_weight — 0.25
// ecs.incident_red_weight — 0.20
// ecs.team_perf_weight — 0.15
// ecs.response_coord_weight — 0.10
function calc_engineer_control_score(float $slaControl, float $taskDist, float $incidentRed, float $teamPerf, float $responseCoord): float {
return round(
(get_coefficient('ecs.sla_weight', 0.30) * $slaControl)
+ (get_coefficient('ecs.task_dist_weight', 0.25) * $taskDist)
+ (get_coefficient('ecs.incident_red_weight', 0.20) * $incidentRed)
+ (get_coefficient('ecs.team_perf_weight', 0.15) * $teamPerf)
+ (get_coefficient('ecs.response_coord_weight', 0.10) * $responseCoord)
, 2);
}
// ========================================================================
// 9. SHS (SYSTEM HEALTH SCORE) — ЗДОРОВЬЕ СИСТЕМЫ
// ========================================================================
// shs.sla_stability_weight — 0.22
// shs.revenue_stability_weight — 0.18
// shs.retention_weight — 0.18
// shs.engineer_perf_weight — 0.15
// shs.incident_stability_weight — 0.12
// shs.sales_flow_weight — 0.10
// shs.operational_eff_weight — 0.05
function calc_shs(float $slaStability, float $revenueStability, float $retention, float $engineerPerformance, float $incidentStability, float $salesFlow, float $operationalEfficiency): float {
return round(
(get_coefficient('shs.sla_stability_weight', 0.22) * $slaStability)
+ (get_coefficient('shs.revenue_stability_weight', 0.18) * $revenueStability)
+ (get_coefficient('shs.retention_weight', 0.18) * $retention)
+ (get_coefficient('shs.engineer_perf_weight', 0.15) * $engineerPerformance)
+ (get_coefficient('shs.incident_stability_weight', 0.12) * $incidentStability)
+ (get_coefficient('shs.sales_flow_weight', 0.10) * $salesFlow)
+ (get_coefficient('shs.operational_eff_weight', 0.05) * $operationalEfficiency)
, 2);
}
/**
* CEO SHS — отдельные веса для CEO-дашборда
* ceo_shs.mrr_weight — 0.25
* ceo_shs.sla_weight — 0.20
* ceo_shs.retention_weight — 0.20
* ceo_shs.productivity_weight — 0.15
* ceo_shs.conversion_weight — 0.10
* ceo_shs.incident_weight — 0.10
*/
function calc_ceo_shs(float $mrrGrowth, float $slaCompliance, float $retention, float $productivity, float $conversion, float $incidentStability): float {
return round(
(get_coefficient('ceo_shs.mrr_weight', 0.25) * $mrrGrowth)
+ (get_coefficient('ceo_shs.sla_weight', 0.20) * $slaCompliance)
+ (get_coefficient('ceo_shs.retention_weight', 0.20) * $retention)
+ (get_coefficient('ceo_shs.productivity_weight', 0.15) * $productivity)
+ (get_coefficient('ceo_shs.conversion_weight', 0.10) * $conversion)
+ (get_coefficient('ceo_shs.incident_weight', 0.10) * $incidentStability)
, 2);
}
/**
* Статус SHS
* shs.zone_growth — 85
* shs.zone_stable — 70
* shs.zone_risk — 50
*/
function shs_status(float $score): string {
if ($score >= get_coefficient('shs.zone_growth', 85)) return 'Growth';
if ($score >= get_coefficient('shs.zone_stable', 70)) return 'Stable';
if ($score >= get_coefficient('shs.zone_risk', 50)) return 'Risk';
return 'Crisis';
}
/**
* Зона SHS с предупреждением
*/
function shs_zone(float $score): array {
if ($score >= get_coefficient('shs.zone_growth', 85))
return ['zone' => 'green', 'label' => 'Growth', 'action' => ''];
if ($score >= get_coefficient('shs.zone_stable', 70))
return ['zone' => 'yellow', 'label' => 'Stable', 'action' => 'Мониторинг'];
if ($score >= get_coefficient('shs.zone_risk', 50))
return ['zone' => 'yellow', 'label' => 'Risk', 'action' => 'Снизить продажи, запустить аудит цикла, пересмотреть назначения'];
return ['zone' => 'red', 'label' => 'Crisis', 'action' => 'Немедленный аудит, приостановка новых продаж, пересмотр команды'];
}
/**
* Дельта SHS (изменение относительно предыдущей записи)
* Если падение > 5 — warning, > 10 — critical
* shs.delta_warning — 5
* shs.delta_critical — 10
*/
function shs_delta(float $currentScore, ?float $previousScore): ?array {
if ($previousScore === null) return null;
$delta = $currentScore - $previousScore;
$thresholdWarn = get_coefficient('shs.delta_warning', 5);
$thresholdCrit = get_coefficient('shs.delta_critical', 10);
if ($delta <= -$thresholdCrit)
return ['delta' => round($delta, 1), 'level' => 'critical', 'message' => 'SHS упал более чем на ' . $thresholdCrit . ' пунктов — кризис'];
if ($delta <= -$thresholdWarn)
return ['delta' => round($delta, 1), 'level' => 'warning', 'message' => 'SHS упал более чем на ' . $thresholdWarn . ' пунктов — требуется внимание'];
return ['delta' => round($delta, 1), 'level' => 'ok', 'message' => ''];
}
// ========================================================================
// 10. SLA STABILITY INDEX (SSI)
// ========================================================================
// Учитывает не только compliance, но и severity нарушений
// SSI = SLA_compliance × (1 - breach_severity)
// ssi.breach_severity_p1 — 1.0
// ssi.breach_severity_p2 — 0.5
// ssi.breach_severity_p3 — 0.2
function calc_sla_stability_index(float $slaCompliance, float $breachSeverityWeighted): float {
$ssi = $slaCompliance * (1 - $breachSeverityWeighted);
return round(max(0, min(100, $ssi)), 2);
}
function calc_breach_severity(int $breachesP1, int $breachesP2, int $breachesP3): float {
$total = $breachesP1 + $breachesP2 + $breachesP3;
if ($total === 0) return 0;
$weighted = $breachesP1 * get_coefficient('ssi.breach_severity_p1', 1.0)
+ $breachesP2 * get_coefficient('ssi.breach_severity_p2', 0.5)
+ $breachesP3 * get_coefficient('ssi.breach_severity_p3', 0.2);
return min(1, $weighted / $total);
}
// ========================================================================
// 11. REVENUE STABILITY INDEX (RSI) через коэффициент вариации
// ========================================================================
// RSI = max(0, 100 × (1 - σ(MRR) / mean(MRR)))
// Основан на исторических значениях MRR за N периодов
function calc_revenue_stability_index(array $mrrHistory): float {
$count = count($mrrHistory);
if ($count < 2) return 100;
$mean = array_sum($mrrHistory) / $count;
if ($mean <= 0) return 100;
$variance = 0;
foreach ($mrrHistory as $mrr) {
$variance += ($mrr - $mean) ** 2;
}
$stdDev = sqrt($variance / $count);
$cv = $stdDev / $mean;
return round(max(0, min(100, 100 * (1 - $cv))), 2);
}
// ========================================================================
// 12. SALES FLOW INDEX (SFI) — ВОРОНКА ПРОДАЖ
// ========================================================================
// SFI = (аудиты → SLA конверсия) × качество лидов
function calc_sales_flow_index(int $audits, int $slaContracts, float $leadQuality = 1.0): float {
if ($audits <= 0) return 0;
$conversion = $slaContracts / $audits;
return round(min(100, $conversion * 100 * $leadQuality), 2);
}
// ========================================================================
// 13. OPERATIONAL EFFICIENCY INDEX (OEI)
// ========================================================================
// OEI = revenue / (engineer_hours × cost_per_hour) × 100
// Нормируется к 100
function calc_operational_efficiency_index(float $revenue, float $engineerHours, float $costPerHour): float {
$total = $engineerHours * $costPerHour;
if ($total <= 0) return 100;
return round(min(100, ($revenue / $total) * 100), 2);
}
// ========================================================================
// 14. INCIDENT STABILITY INDEX (ISI) с весами severity
// ========================================================================
// ISI = max(0, 100 - (incidents / objects × severity_weight))
// isi.severity_p1 — 1.0
// isi.severity_p2 — 0.5
// isi.severity_p3 — 0.2
function calc_incident_stability_index(int $incidentsP1, int $incidentsP2, int $incidentsP3, int $objectCount): float {
if ($objectCount <= 0) return 100;
$weighted = $incidentsP1 * get_coefficient('isi.severity_p1', 1.0)
+ $incidentsP2 * get_coefficient('isi.severity_p2', 0.5)
+ $incidentsP3 * get_coefficient('isi.severity_p3', 0.2);
$rate = $weighted / $objectCount;
return round(max(0, min(100, 100 - ($rate * 20))), 2);
}
// ========================================================================
// 15. AUTOMATION RULES (АВТОМАТИЧЕСКИЕ ПРАВИЛА)
// ========================================================================
/**
* Проверить все правила автоматизации.
* Возвращает массив предупреждений.
*
* Правила из docs/Цифровая модель общая.md:
* 1. IF SHS < 70 → снизить продажи, запустить аудит, пересмотреть назначения
* 2. IF SLA < 90% → заморозить неприоритетные проекты, увеличить частоту проверок
* 3. IF retention < 90% → обязательный аудит клиентов, пересмотр назначений
*
* rules.shs_threshold — 70
* rules.sla_threshold — 90
* rules.retention_threshold — 90
*/
function check_automation_rules(?float $shs, ?float $slaCompliance, ?float $retention): array {
$alerts = [];
if ($shs !== null && $shs < get_coefficient('rules.shs_threshold', 70)) {
$alerts[] = [
'rule' => 'SHS',
'severity' => 'critical',
'message' => 'SHS ниже ' . get_coefficient('rules.shs_threshold', 70) . ' — снизить продажи, запустить аудит, пересмотреть назначения инженеров.',
];
}
if ($slaCompliance !== null && $slaCompliance < get_coefficient('rules.sla_threshold', 90)) {
$alerts[] = [
'rule' => 'SLA',
'severity' => 'warning',
'message' => 'SLA Compliance ниже ' . get_coefficient('rules.sla_threshold', 90) . '% — заморозить неприоритетные проекты, увеличить частоту проверок.',
];
}
if ($retention !== null && $retention < get_coefficient('rules.retention_threshold', 90)) {
$alerts[] = [
'rule' => 'Retention',
'severity' => 'critical',
'message' => 'Удержание клиентов ниже ' . get_coefficient('rules.retention_threshold', 90) . '% — обязательный аудит клиентов, пересмотр назначений инженеров.',
];
}
return $alerts;
}
// ========================================================================
// 16. БАЗОВЫЕ KPI
// ========================================================================
/** SLA Compliance: процент задач, закрытых в срок */
function calc_sla_compliance(int $closedInSla, int $total): float {
if ($total <= 0) return 100;
return round(($closedInSla / $total) * 100, 2);
}
/** Reopen Rate: процент повторных обращений */
function calc_reopen_rate(int $reopened, int $total): float {
if ($total <= 0) return 0;
return round(($reopened / $total) * 100, 2);
}
/** Diagnosis Accuracy: точность диагностики */
function calc_diagnosis_accuracy(int $confirmed, int $found): float {
if ($found <= 0) return 100;
return round(($confirmed / $found) * 100, 2);
}
/** Response Time Score: оценка времени реакции */
function calc_response_time_score(float $actualHours, float $slaHours): float {
if ($slaHours <= 0) return 100;
$ratio = $actualHours / $slaHours;
if ($ratio <= 1.0) return 100;
if ($ratio <= 1.2) return 80;
return max(0, 100 - ($ratio - 1.2) * 50);
}
/** Resolution Time Score: оценка времени устранения */
function calc_resolution_time_score(float $actualHours, float $normHours): float {
if ($normHours <= 0) return 100;
$ratio = $actualHours / $normHours;
if ($ratio <= 1.0) return 100;
if ($ratio <= 1.5) return 70;
return max(0, 100 - ($ratio - 1.5) * 40);
}
/** Risk Coverage Score: покрытие зон проверки */
function calc_risk_coverage(int $checkedZones, int $totalZones = 5): float {
if ($totalZones <= 0) return 0;
return round(($checkedZones / $totalZones) * 100, 2);
}
/** Documentation Quality: полнота отчётов */
function calc_documentation_quality(int $fullReports, int $totalVisits): float {
if ($totalVisits <= 0) return 100;
return round(($fullReports / $totalVisits) * 100, 2);
}
/** Team Load Balance: равномерность загрузки (stdDev) — чем меньше, тем лучше */
function calc_tlb(array $tasksPerEngineer): float {
$count = count($tasksPerEngineer);
if ($count < 2) return 0;
$mean = array_sum($tasksPerEngineer) / $count;
$variance = 0;
foreach ($tasksPerEngineer as $t) {
$variance += ($t - $mean) ** 2;
}
return round(sqrt($variance / $count), 2);
}
// ========================================================================
// 17. ФИНАНСОВЫЕ ПОКАЗАТЕЛИ
// ========================================================================
/** MRR: месячный повторяющийся доход */
function calc_mrr(float $totalSlaAnnual): float {
return round($totalSlaAnnual / 12, 2);
}
/** Revenue per Engineer */
function calc_rpe(float $slaRevenue, int $engineerCount): float {
if ($engineerCount <= 0) return 0;
return round($slaRevenue / $engineerCount, 2);
}
/** Cost per SLA Object (CPO) */
function calc_cpo(float $totalCost, int $objectCount): float {
if ($objectCount <= 0) return 0;
return round($totalCost / $objectCount, 2);
}
/** Average Revenue Per Client (ARPC) */
function calc_arpc(float $totalRevenue, int $clientCount): float {
if ($clientCount <= 0) return 0;
return round($totalRevenue / $clientCount, 2);
}
// ========================================================================
// 18. ОПЕРАЦИОННЫЕ KPI
// ========================================================================
/** Utilization: загрузка сотрудника */
function calc_utilization(int $workingHours, int $availableHours): float {
if ($availableHours <= 0) return 0;
return round(($workingHours / $availableHours) * 100, 2);
}
/** Autonomy: самостоятельность */
function calc_autonomy(int $independent, int $total): float {
if ($total <= 0) return 100;
return round(($independent / $total) * 100, 2);
}
/** Escalation Rate */
function calc_escalation_rate(int $escalated, int $total): float {
if ($total <= 0) return 0;
return round(($escalated / $total) * 100, 2);
}
/** System Uptime: доступность системы */
function calc_uptime(float $uptimeHours, float $totalHours): float {
if ($totalHours <= 0) return 100;
return round(($uptimeHours / $totalHours) * 100, 2);
}
// ========================================================================
// 19. КЛИЕНТСКИЕ МЕТРИКИ
// ========================================================================
/** Retention: удержание клиентов */
function calc_retention(int $activeClients, int $totalClients): float {
if ($totalClients <= 0) return 100;
return round(($activeClients / $totalClients) * 100, 2);
}
/** Retention с штрафом за потерю ключевых клиентов */
function calc_retention_key_client(float $baseRetention, int $lostKeyClients): float {
$penalty = $lostKeyClients * get_coefficient('retention.key_client_penalty', 0.1);
return round(max(0, $baseRetention - $penalty * 100), 2);
}
/** CSAT (удовлетворённость клиентов) */
function calc_csat(int $positive, int $total): float {
if ($total <= 0) return 100;
return round(($positive / $total) * 100, 2);
}
// ========================================================================
// 20. ИНЦИДЕНТЫ
// ========================================================================
/** Incident Rate: количество инцидентов на объект */
function calc_incident_rate(int $incidents, int $objectCount): float {
if ($objectCount <= 0) return 0;
return round($incidents / $objectCount, 2);
}
/** Incident Reduction Rate: снижение инцидентов */
/** Incident Reduction Rate */
function calc_incident_reduction(int $previous, int $current): float {
if ($previous <= 0) return 0;
return round((($previous - $current) / $previous) * 100, 2);
}
// ========================================================================
// 21. БОНУСНАЯ СИСТЕМА
// ========================================================================
// bonus.score_90plus — +20%
// bonus.score_80_89 — +10%
// bonus.score_below_80 — 0%
function calc_bonus_percent(float $engineerScore): float {
if ($engineerScore >= get_coefficient('bonus.score_90plus', 90)) return get_coefficient('bonus.premium_90plus', 20);
if ($engineerScore >= get_coefficient('bonus.score_80_89', 80)) return get_coefficient('bonus.premium_80_89', 10);
return get_coefficient('bonus.premium_below_80', 0);
}