"""Производственный календарь (D30): загрузка года из файла xmlcalendar.ru.""" from __future__ import annotations import logging from datetime import date import streamlit as st from app.db.base import run_db from app.imports.xmlcalendar import ( SOURCE, CalendarFormatError, ParsedCalendar, parse_calendar_json, ) from app.services import business_calendar from app.ui.common import as_table, call_db, current_user logger = logging.getLogger(__name__) # Разобранный файл (ParsedCalendar) или текст ошибки — кладётся при загрузке. _UPLOAD_KEY = "calendar_upload" # Сброс поля загрузки после сохранения: у виджета меняется ключ. _NONCE_KEY = "calendar_file_nonce" _YEAR_COLUMNS = { "year": "Год", "non_working": "Нерабочих дней", "weekday_holidays": "Праздники и переносы в будни", "working_weekends": "Рабочие выходные", "updated_by": "Кто загрузил", "updated_at": "Когда", } def render() -> None: st.header("Производственный календарь") st.caption( f"Файл на год с {SOURCE}: https://xmlcalendar.ru/data/ru/ГГГГ/calendar.json. " "От календаря считается дата отчёта по умолчанию — предыдущий рабочий " "день. Для года без календаря рабочими считаются будни. Повторная " "загрузка года заменяет его целиком." ) years = call_db(business_calendar.list_years(), "Не удалось загрузить календарь") if years is None: return loaded = {row.year for row in years} if date.today().year not in loaded: st.warning( f"Календарь на {date.today().year} не загружен: праздники при выборе " "даты отчёта не учитываются." ) widget_key = f"calendar_file_{st.session_state.get(_NONCE_KEY, 0)}" st.file_uploader( "Файл календаря (.json)", type=["json"], key=widget_key, on_change=_on_upload, args=(widget_key,), ) upload = st.session_state.get(_UPLOAD_KEY) if isinstance(upload, str): st.error(upload) elif isinstance(upload, ParsedCalendar): _render_preview(upload, loaded) st.subheader("Загруженные годы") if years: st.dataframe(as_table(years, _YEAR_COLUMNS), hide_index=True) else: st.caption("Календарь пока не загружен.") def _on_upload(widget_key: str) -> None: file = st.session_state.get(widget_key) if file is None: st.session_state.pop(_UPLOAD_KEY, None) return try: st.session_state[_UPLOAD_KEY] = parse_calendar_json(file.getvalue()) except CalendarFormatError as exc: st.session_state[_UPLOAD_KEY] = str(exc) def _render_preview(parsed: ParsedCalendar, loaded: set[int]) -> None: st.write( f"Год {parsed.year}: нерабочих дней {len(parsed.non_working)}, " f"сокращённых {len(parsed.shortened)}." ) st.write(f"Праздники и переносы в будни: {_dates(parsed.weekday_holidays)}.") st.write(f"Рабочие выходные: {_dates(parsed.working_weekends)}.") if parsed.year in loaded: st.warning(f"Календарь на {parsed.year} уже загружен — он будет заменён.") if st.button(f"Сохранить календарь на {parsed.year}", type="primary"): _save(parsed) def _dates(days: tuple[date, ...]) -> str: return ", ".join(f"{d:%d.%m}" for d in days) if days else "нет" def _save(parsed: ParsedCalendar) -> None: try: run_db(business_calendar.save_year(parsed, current_user())) except Exception as exc: # noqa: BLE001 logger.exception("Ошибка сохранения календаря на %d", parsed.year) st.error(f"Не удалось сохранить календарь: {exc}") return st.session_state.pop(_UPLOAD_KEY, None) st.session_state[_NONCE_KEY] = st.session_state.get(_NONCE_KEY, 0) + 1 st.toast(f"Календарь на {parsed.year} сохранён.") st.rerun()