Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View File

@@ -0,0 +1,289 @@
# -*- 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 ""