Files
Dispatch/Dispatch_V0.1.1/ui/task_view_formatters.py
2026-04-29 08:18:54 +04:00

290 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 ""