290 lines
12 KiB
Python
290 lines
12 KiB
Python
# -*- 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 ""
|