# -*- coding: utf-8 -*- # hub/ticket/ui/task_view_formatters.py """Форматирование отображения задач Ticket для карточек и деталей.""" from __future__ import annotations from datetime import datetime from pathlib import Path from domain import TicketTaskSnapshot, parse_location_parts from domain.ticket_constants import ( STATE_COMPLETED, STATE_CONFIRMATION, STATE_IN_PROGRESS, STATE_REFUSED, STATE_TODO, ) _REPO_ROOT = Path(__file__).resolve().parents[3] _TICKET_ICONS_DIR = _REPO_ROOT / "gui" / "icons" _TICKET_SPECIALISTS_DIR = _TICKET_ICONS_DIR / "specialists" _DEFAULT_SPECIALIST_PHOTO = "specialist.png" _DEFAULT_STAGE_ICON = "close.png" _STAGE_ICON_FILENAMES = { "specialist": "face-man-shimmer-outline.png", "diagnostic": "wrench-outline.png", "repair": "wrench-outline.png", "acceptance": "pencil-outline.png", } _SPECIALIST_CARD_INFO = { "Иванов Алексей Сергеевич": ("Иванов А.С.", "Инженер-электроник", "specialist1.png"), "Петрова Мария Владимировна": ("Петрова М.В.", "Техник-рентгенолог", "specialist2.png"), "Сидоров Дмитрий Иванович": ("Сидоров Д.И.", "Инженер КИПиА", "specialist3.png"), "Козлова Анна Петровна": ("Козлова А.П.", "Специалист по ТО", "specialist4.png"), "Васильев Сергей Николаевич": ("Васильев С.Н.", "Инженер-программист", "specialist5.png"), "Николаева Ольга Дмитриевна": ("Николаева О.Д.", "Техник-электрик", "specialist6.png"), "Фёдоров Андрей Викторович": ("Фёдоров А.В.", "Инженер-механик", "specialist7.png"), "Орлова Екатерина Александровна": ("Орлова Е.А.", "Специалист по диагностике", "specialist8.png"), } def build_task_title(task: TicketTaskSnapshot) -> str: if task.sequence_number: return f"Задача #{task.sequence_number}" return f"Задача #{task.task_id}" def build_task_card_subtitle(task: TicketTaskSnapshot) -> str: specialist_text = task.assigned_specialist.strip() or "Специалист не назначен" return "\n".join( ( task.location or "Локация не указана", specialist_text, build_documents_summary(task), ) ) def build_documents_summary(task: TicketTaskSnapshot) -> str: return ( f"Д: {_bool_text(task.diagnostic_report_signed)} | " f"Р: {_bool_text(task.repair_report_signed)} | " f"А: {_bool_text(task.acceptance_report_signed)}" ) def split_task_location(location: str) -> tuple[str, str, str]: normalized_location = (location or "").strip() if not normalized_location: return ("Локация не указана", "Аппарат не указан", "Кабинет не указан") institution, room, device = parse_location_parts(normalized_location) institution = institution or "Локация не указана" device = device or "Аппарат не указан" room = room or "Кабинет не указан" return ( shorten_card_text(institution, limit=36), shorten_card_text(device, limit=34), shorten_card_text(room, limit=24), ) def build_task_footer_text(task: TicketTaskSnapshot) -> str: return build_specialist_card_text(task.assigned_specialist) def build_task_fault_title(task: TicketTaskSnapshot) -> str: """Извлечь краткий заголовок неисправности из строки локации задачи. Формат локации, формируемый формой создания заявки: «Учреждение (Аппарат — Краткий заголовок, каб. №)». Парсер `parse_location_parts` возвращает блок «Аппарат — Краткий заголовок» в позиции `device`. Заголовок неисправности — это содержимое после разделителя « — ». При его отсутствии (например, для тестовых задач без формы) возвращается само устройство, а в предельном случае — оригинальная строка локации. """ institution, _device, _room = split_task_location(task.location) _, _, raw_device = parse_location_parts(task.location or "") device_text = raw_device.strip() or institution separator = " — " if separator in device_text: title = device_text.split(separator, 1)[1].strip() if title: return shorten_card_text(title, limit=64) if device_text: return shorten_card_text(device_text, limit=64) return shorten_card_text(task.location or "Заголовок не указан", limit=64) def build_task_stage_flags(task: TicketTaskSnapshot) -> tuple[tuple[str, bool], ...]: return ( ("specialist", bool(task.assigned_specialist.strip())), ("diagnostic", bool(task.diagnostic_report_signed)), ("repair", bool(task.repair_report_signed)), ("acceptance", bool(task.acceptance_report_signed)), ) def build_specialist_initials(specialist_name: str) -> str: normalized_name = (specialist_name or "").strip() if not normalized_name: return "?" parts = [part for part in normalized_name.split() if part] if len(parts) >= 2: return f"{parts[0][0]}{parts[1][0]}".upper() if len(parts[0]) >= 2: return parts[0][:2].upper() return parts[0][:1].upper() def build_specialist_card_text(specialist_name: str) -> str: specialist_info = build_specialist_card_info(specialist_name) short_name = specialist_info["short_name"] position = specialist_info["position"] if not short_name: return "Специалист не назначен" if not position: return shorten_card_text(short_name, limit=40) return shorten_card_text(f"{short_name} • {position}", limit=42) def build_specialist_card_info(specialist_name: str) -> dict[str, str]: normalized_name = (specialist_name or "").strip() if not normalized_name: return {"short_name": "", "position": "", "photo": ""} short_name, position, photo_filename = _SPECIALIST_CARD_INFO.get( normalized_name, _fallback_specialist_card_info(normalized_name), ) return { "short_name": short_name, "position": position, "photo": photo_filename, } def build_specialist_photo_path(specialist_name: str, photo_filename: str) -> str: normalized_filename = (photo_filename or "").strip() if normalized_filename: return _resolve_specialist_photo_path(normalized_filename) specialist_info = build_specialist_card_info(specialist_name) photo_from_mapping = specialist_info.get("photo", "").strip() if not photo_from_mapping: return "" return _resolve_specialist_photo_path(photo_from_mapping) def build_stage_icon_path(stage_key: str, is_active: bool) -> str: filename = _STAGE_ICON_FILENAMES.get(stage_key, _DEFAULT_STAGE_ICON) if is_active else _DEFAULT_STAGE_ICON icon_path = _TICKET_ICONS_DIR / filename if not icon_path.exists(): return "" return str(icon_path) def shorten_card_text(text: str, limit: int) -> str: normalized_text = " ".join((text or "").split()) if len(normalized_text) <= limit: return normalized_text if limit <= 3: return normalized_text[:limit] return f"{normalized_text[:limit - 3].rstrip()}..." def build_stage_rows(task: TicketTaskSnapshot) -> tuple[tuple[str, str], ...]: specialist_status = ( f"Назначен: {task.assigned_specialist}" if task.assigned_specialist.strip() else "Не назначен" ) diagnostic_status = ( "Диагностический отчёт подписан" if task.diagnostic_report_signed else "Диагностический отчёт ожидается" ) repair_status = ( "Ремонтный отчёт подписан" if task.repair_report_signed else "Ремонтный отчёт ожидается" ) acceptance_status = ( "Акт приёмки подписан" if task.acceptance_report_signed else "Акт приёмки ожидается" ) return ( ("Специалист", specialist_status), ("Диагностика", diagnostic_status), ("Ремонт", repair_status), ("Приёмка", acceptance_status), ) def build_action_hint(task: TicketTaskSnapshot) -> str: if task.state_code == STATE_TODO: return "Назначьте специалиста и переведите задачу аппаратной кнопкой в работу." if task.state_code == STATE_IN_PROGRESS: if not task.assigned_specialist.strip(): return "Сначала назначьте специалиста, затем подпишите диагностический и ремонтный отчёты." if not task.diagnostic_report_signed or not task.repair_report_signed: return "Для перехода к подтверждению подпишите диагностический и ремонтный отчёты." return "Отчёты готовы. Следующий аппаратный переход переведёт задачу к подтверждению." if task.state_code == STATE_CONFIRMATION: if not task.acceptance_report_signed: return "Подпишите акт приёмки, чтобы задача могла перейти в выполненные." return "Акт готов. Следующий аппаратный переход завершит задачу." if task.state_code == STATE_COMPLETED: return "Задача завершена. Её можно переместить в архив." if task.state_code == STATE_REFUSED: return "Задача завершилась отказом. Её можно переместить в архив." return "Архивная запись доступна только для просмотра." def format_datetime(value: datetime | None) -> str: if value is None: return "—" return value.strftime("%d.%m.%Y %H:%M") def can_assign_specialist(task: TicketTaskSnapshot) -> bool: return task.state_code in {STATE_TODO, STATE_IN_PROGRESS, STATE_CONFIRMATION} def can_sign_diagnostic(task: TicketTaskSnapshot) -> bool: return ( task.state_code == STATE_IN_PROGRESS and bool(task.assigned_specialist.strip()) and not task.diagnostic_report_signed ) def can_sign_repair(task: TicketTaskSnapshot) -> bool: return ( task.state_code == STATE_IN_PROGRESS and bool(task.assigned_specialist.strip()) and not task.repair_report_signed ) def can_sign_acceptance(task: TicketTaskSnapshot) -> bool: return task.state_code == STATE_CONFIRMATION and not task.acceptance_report_signed def can_archive(task: TicketTaskSnapshot) -> bool: return task.state_code in {STATE_COMPLETED, STATE_REFUSED} def _bool_text(value: bool) -> str: return "Да" if value else "Нет" def _fallback_specialist_card_info(specialist_name: str) -> tuple[str, str, str]: parts = [part for part in specialist_name.split() if part] if len(parts) >= 3: initials = f"{parts[1][0]}.{parts[2][0]}." return (f"{parts[0]} {initials}", "Специалист", "") return (specialist_name, "Специалист", "") def _resolve_specialist_photo_path(photo_filename: str) -> str: photo_path = _TICKET_SPECIALISTS_DIR / photo_filename if photo_path.exists(): return str(photo_path) fallback_path = _TICKET_SPECIALISTS_DIR / _DEFAULT_SPECIALIST_PHOTO if fallback_path.exists(): return str(fallback_path) return ""