From b062237168cc4061c9002a863346ee39c17dc1d5 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Sat, 6 Jun 2026 19:47:24 +0300 Subject: [PATCH] fix(worktime): hide action-only ops from workforce rollups --- adk-rust/crates/worktime-api/src/main.rs | 158 ++++++++++++++++++++--- 1 file changed, 137 insertions(+), 21 deletions(-) diff --git a/adk-rust/crates/worktime-api/src/main.rs b/adk-rust/crates/worktime-api/src/main.rs index 2b60a71..d358d11 100644 --- a/adk-rust/crates/worktime-api/src/main.rs +++ b/adk-rust/crates/worktime-api/src/main.rs @@ -2118,26 +2118,9 @@ fn build_rollups(rows: &[Value], actions: &[Value], field: &str) -> Vec { .filter(|s| !s.is_empty()) .unwrap_or("unassigned") .to_string(); - let group = groups.entry(name.clone()).or_insert_with(|| { - let mut g = Map::new(); - g.insert("name".into(), json!(name)); - for key in [ - "users_count", - "active_users", - "inactive_users", - "below_target_users", - "workday_total_active_seconds", - "actions_count", - "critical_actions_count", - "high_actions_count", - "medium_actions_count", - "low_actions_count", - ] { - g.insert(key.into(), json!(0)); - } - g.insert("users".into(), json!([])); - g - }); + let Some(group) = groups.get_mut(&name) else { + continue; + }; inc(group, "actions_count", 1); let key = match action.get("priority").and_then(Value::as_str).unwrap_or("") { "critical" => "critical_actions_count", @@ -2178,6 +2161,12 @@ fn inc(map: &mut Map, key: &str, amount: i64) { fn compact_rollup_points(rollups: &[Value]) -> Vec { rollups .iter() + .filter(|item| { + item.get("users_count") + .and_then(Value::as_i64) + .unwrap_or(0) + > 0 + }) .map(|item| { json!({ "name": item.get("name").cloned().unwrap_or(json!("")), @@ -2405,6 +2394,10 @@ fn add_rollup_history_insights( continue; }; for item in items { + let users = item.get("users_count").and_then(Value::as_i64).unwrap_or(0); + if users <= 0 { + continue; + } let Some(name) = item.get("name").and_then(Value::as_str) else { continue; }; @@ -2909,7 +2902,20 @@ fn history_component(value: &str) -> String { fn load_management_history_point(path: &Path) -> Option { let payload: Value = serde_json::from_slice(&fs::read(path).ok()?).ok()?; - payload.get("point").cloned() + payload.get("point").cloned().map(sanitize_trend_point) +} + +fn sanitize_trend_point(mut point: Value) -> Value { + let Some(object) = point.as_object_mut() else { + return point; + }; + for key in ["department_rollups", "owner_rollups"] { + let Some(items) = object.get(key).and_then(Value::as_array) else { + continue; + }; + object.insert(key.to_string(), json!(compact_rollup_points(items))); + } + point } fn today_csv(rows: &[Value]) -> String { @@ -3361,6 +3367,116 @@ mod tests { ); } + #[test] + fn management_history_ignores_action_only_rollups() { + let cfg = test_config(); + let trend = ["2026-05-12", "2026-05-13", "2026-05-14"] + .into_iter() + .map(|date| { + json!({ + "report_date": date, + "portfolio_coverage_pct": 50.0, + "department_rollups": [ + {"name": "ops", "users_count": 0, "portfolio_coverage_pct": 0.0}, + {"name": "Отдел продаж", "users_count": 2, "portfolio_coverage_pct": 10.0} + ], + "owner_rollups": [ + {"name": "ops", "users_count": 0, "portfolio_coverage_pct": 0.0}, + {"name": "Руководитель", "users_count": 1, "portfolio_coverage_pct": 10.0} + ] + }) + }) + .collect::>(); + let insights = build_management_insights( + &cfg, + &json!({ + "calendar_total_active_seconds": 0, + "workday_total_active_seconds": 0 + }), + &[], + &[], + &[], + &trend, + NaiveDate::from_ymd_opt(2026, 5, 14).unwrap(), + ); + assert!(!insights.iter().any(|item| item["subject"] == "ops")); + assert!( + insights + .iter() + .any(|item| item["subject"] == "Отдел продаж") + ); + assert!( + insights + .iter() + .any(|item| item["subject"] == "Руководитель") + ); + } + + #[test] + fn compact_rollup_points_hides_action_only_groups() { + let points = compact_rollup_points(&[ + json!({"name": "ops", "users_count": 0, "portfolio_coverage_pct": 0.0, "actions_count": 2}), + json!({"name": "Отдел продаж", "users_count": 2, "portfolio_coverage_pct": 50.0, "actions_count": 1}), + ]); + assert_eq!(points.len(), 1); + assert_eq!(points[0]["name"], "Отдел продаж"); + } + + #[test] + fn build_rollups_does_not_create_action_only_groups() { + let rows = vec![json!({ + "user": "user1", + "department": "Отдел продаж", + "manager_owner": "Руководитель", + "status": "below_target", + "workday_active_seconds": 3600 + })]; + let actions = vec![ + action( + "source_freshness_review", + "critical", + "ops", + "today", + "техническая проверка источника", + "проверить сервис", + "", + json!({}), + ), + action( + "manager_review", + "high", + "Руководитель", + "today", + "проверка руководителя", + "разобрать нагрузку", + "", + json!({}), + ), + ]; + let rollups = build_rollups(&rows, &actions, "manager_owner"); + assert_eq!(rollups.len(), 1); + assert_eq!(rollups[0]["name"], "Руководитель"); + assert_eq!(rollups[0]["actions_count"], 1); + } + + #[test] + fn sanitize_trend_point_removes_legacy_action_only_rollups() { + let point = sanitize_trend_point(json!({ + "department_rollups": [ + {"name": "ops", "users_count": 0, "portfolio_coverage_pct": 0.0}, + {"name": "Отдел продаж", "users_count": 2, "portfolio_coverage_pct": 50.0} + ], + "owner_rollups": [ + {"name": "ops", "users_count": 0, "portfolio_coverage_pct": 0.0}, + {"name": "Руководитель", "users_count": 1, "portfolio_coverage_pct": 50.0} + ] + })); + assert_eq!(point["department_rollups"].as_array().unwrap().len(), 1); + assert_eq!(point["owner_rollups"].as_array().unwrap().len(), 1); + assert_eq!(point["department_rollups"][0]["name"], "Отдел продаж"); + assert_eq!(point["owner_rollups"][0]["name"], "Руководитель"); + } + #[test] fn interpretation_policy_accepts_fraction_thresholds() { let policy: InterpretationPolicy = serde_json::from_value(json!({