feat(web): стабилизация UI, Gemini-скроллинг, роутеры контекста/снапшотов и актуализация роадмапа

This commit is contained in:
2026-09-07 17:29:21 +03:00
parent 1bb95cd8e1
commit d391a08224
33 changed files with 7768 additions and 1832 deletions
+74 -16
View File
@@ -7,6 +7,7 @@ ROLE: Надежная загрузка штата и отсутствий (MS S
import os
import warnings
from datetime import datetime # <-- ДОБАВИТЬ ЭТУ СТРОКУ
import pandas as pd
from config import (
normalize_fio, clean_scud_fio_light, load_exceptions,
@@ -101,27 +102,84 @@ def load_absent_data(date_str):
if df_absent is None:
df_absent = pd.DataFrame(columns=['fio_clean', 'Вид_отсутствия'])
# Обогащение удаленщиками из static_reason_workers.csv
# Обогащение удаленщиками из static_reason_workers.csv с ротацией просроченных записей
try:
static_path = os.path.join(DATA_DIR, "static_reason_workers.csv")
if os.path.exists(static_path):
df_static = pd.read_csv(static_path, encoding='utf-8')
if 'fio' in df_static.columns and 'reason' in df_static.columns:
df_static['fio_clean'] = df_static['fio'].apply(normalize_fio)
existing_fios = set(df_absent['fio_clean'].dropna().tolist()) if not df_absent.empty else set()
new_rows = []
for _, s_row in df_static.iterrows():
if s_row['fio_clean'] not in existing_fios:
new_rows.append({
'fio_clean': s_row['fio_clean'],
'Вид_отсутствия': s_row['reason']
df_static = pd.read_csv(static_path, dtype=str, on_bad_lines='skip').fillna("")
for col in ['fio', 'reason', 'department', 'date_from', 'date_to']:
if col not in df_static.columns:
df_static[col] = ""
today_date = datetime.now().date()
# Разбор целевой даты отчета
clean_target_str = str(date_str).replace('_', '.')
try:
clean_target_date = datetime.strptime(clean_target_str, "%d.%m.%Y").date()
except ValueError:
clean_target_date = today_date
def parse_date_safe(d_val):
if not d_val or str(d_val).lower() in ['nan', 'none', '', 'nat']:
return None
s = str(d_val).strip().replace('_', '.')
for fmt in ("%d.%m.%Y", "%Y-%m-%d"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
pass
return None
active_for_file = []
new_rows_for_report = []
file_changed = False
existing_fios = set(df_absent['fio_clean'].dropna().tolist()) if not df_absent.empty else set()
for _, s_row in df_static.iterrows():
fio_raw = str(s_row.get('fio', '')).strip()
if not fio_raw:
continue
fio_c = normalize_fio(fio_raw)
reason = str(s_row.get('reason', '')).strip() or "Дистанционная работа"
d_from = parse_date_safe(s_row.get('date_from'))
d_to = parse_date_safe(s_row.get('date_to'))
# 1. Физическая ротация просроченных: только если текущий реальный день (today) строго больше date_to
if d_to is not None and today_date > d_to:
print(f" [🧹] Удаленка истекла: {fio_raw} (до {d_to.strftime('%d.%m.%Y')}). Удалена из CSV.")
file_changed = True
continue
active_for_file.append(s_row.to_dict())
# 2. Проверка действия удаленки на дату формируемого отчета:
# Если date_from не указана — действует всегда до date_to
is_after_start = (d_from is None) or (clean_target_date >= d_from)
is_before_end = (d_to is None) or (clean_target_date <= d_to)
if is_after_start and is_before_end:
if fio_c not in existing_fios:
new_rows_for_report.append({
'fio_clean': fio_c,
'Вид_отсутствия': reason
})
if new_rows:
df_absent = pd.concat([df_absent, pd.DataFrame(new_rows)], ignore_index=True)
print(f" [✓] Реестр удаленщиков: добавлено {len(new_rows)} чел. из static_reason_workers.csv")
existing_fios.add(fio_c)
# Перезаписываем CSV только если реально были удалены просроченные сотрудники
if file_changed:
pd.DataFrame(active_for_file).to_csv(static_path, index=False, encoding='utf-8')
print(" [✓] Файл static_reason_workers.csv синхронизирован без просроченных записей.")
if new_rows_for_report:
df_absent = pd.concat([df_absent, pd.DataFrame(new_rows_for_report)], ignore_index=True)
print(f" [✓] Реестр удаленщиков: добавлено {len(new_rows_for_report)} чел. в отчет за {date_str}")
except Exception as e:
print(f" [⚠️] Ошибка чтения static_reason_workers.csv: {e}")
print(f" [⚠️] Ошибка обработки static_reason_workers.csv: {e}")
return df_absent if not df_absent.empty else None