Add Dispatch_V0.1.1
This commit is contained in:
17
Dispatch_V0.1.1/ui/dialogs/__init__.py
Normal file
17
Dispatch_V0.1.1/ui/dialogs/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/__init__.py
|
||||
|
||||
"""Диалоги Ticket."""
|
||||
|
||||
from .acceptance_dialog import AcceptanceDialog
|
||||
from .specialist_dialog import SpecialistDialog
|
||||
from .task_refusal_dialog import TaskRefusalDialog
|
||||
from .report_dialogs import DiagnosticReportDialog, RepairReportDialog
|
||||
|
||||
__all__ = [
|
||||
"AcceptanceDialog",
|
||||
"DiagnosticReportDialog",
|
||||
"RepairReportDialog",
|
||||
"SpecialistDialog",
|
||||
"TaskRefusalDialog",
|
||||
]
|
||||
BIN
Dispatch_V0.1.1/ui/dialogs/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Dispatch_V0.1.1/ui/dialogs/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
86
Dispatch_V0.1.1/ui/dialogs/acceptance_dialog.py
Normal file
86
Dispatch_V0.1.1/ui/dialogs/acceptance_dialog.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/acceptance_dialog.py
|
||||
|
||||
"""UI-диалог акта приёмки Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gui.containers import SContainer
|
||||
|
||||
from domain import TicketTaskSnapshot
|
||||
from .base_document_dialog import BaseDocumentDialog
|
||||
|
||||
|
||||
class AcceptanceDialog(BaseDocumentDialog):
|
||||
"""Диалог подписания акта приёмки."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
):
|
||||
self._work_description = None
|
||||
self._executor_signature = None
|
||||
self._customer_signature = None
|
||||
super().__init__(
|
||||
task=task,
|
||||
title="Акт приёмки",
|
||||
submit_text="Подписать акт",
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
def build_payload(self) -> dict[str, str]:
|
||||
return {
|
||||
"work_description": self._work_description.get_text().strip(),
|
||||
"executor_signature": self._executor_signature.get_text().strip(),
|
||||
"customer_signature": self._customer_signature.get_text().strip(),
|
||||
}
|
||||
|
||||
def _is_ready(self) -> bool:
|
||||
payload = self.build_payload()
|
||||
return bool(
|
||||
payload["work_description"]
|
||||
and payload["executor_signature"]
|
||||
and payload["customer_signature"]
|
||||
)
|
||||
|
||||
def _build_form(self, container: SContainer) -> None:
|
||||
work_description_shell = SContainer(
|
||||
height_percent=40,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._work_description = self._populate_text_block(
|
||||
work_description_shell,
|
||||
"Описание выполненных работ",
|
||||
"Опишите объём работ, который передаётся заказчику.",
|
||||
)
|
||||
container.add_widget(work_description_shell)
|
||||
|
||||
executor_signature_shell = SContainer(
|
||||
height_percent=23,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._executor_signature = self._populate_text_block(
|
||||
executor_signature_shell,
|
||||
"Исполнитель",
|
||||
"Укажите ФИО и должность исполнителя.",
|
||||
)
|
||||
container.add_widget(executor_signature_shell)
|
||||
|
||||
customer_signature_shell = SContainer(
|
||||
height_percent=23,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._customer_signature = self._populate_text_block(
|
||||
customer_signature_shell,
|
||||
"Заказчик",
|
||||
"Укажите ФИО и должность представителя заказчика.",
|
||||
)
|
||||
container.add_widget(customer_signature_shell)
|
||||
self._refresh_submit_state()
|
||||
218
Dispatch_V0.1.1/ui/dialogs/base_document_dialog.py
Normal file
218
Dispatch_V0.1.1/ui/dialogs/base_document_dialog.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/base_document_dialog.py
|
||||
|
||||
"""Базовый UI-диалог документов Ticket без файловой и доменной логики."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gui.components import Button, Dialog, Label, TextInput
|
||||
from gui.containers import HContainer, SContainer, VContainer
|
||||
|
||||
from domain import TicketTaskSnapshot, parse_location_parts
|
||||
from ui.task_view_formatters import build_specialist_card_info
|
||||
|
||||
|
||||
class BaseDocumentDialog(Dialog):
|
||||
"""Базовый modal-диалог для ввода данных документа Ticket."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
title: str,
|
||||
submit_text: str,
|
||||
parent=None,
|
||||
):
|
||||
self._task = task
|
||||
self._cancel_button: Button | None = None
|
||||
self._submit_button: Button | None = None
|
||||
self._form_text_edits: list[TextInput] = []
|
||||
super().__init__(
|
||||
title=title,
|
||||
width=540,
|
||||
height=740,
|
||||
modal=True,
|
||||
parent=parent,
|
||||
)
|
||||
self._setup_ui(submit_text)
|
||||
self._connect_signals()
|
||||
self._refresh_submit_state()
|
||||
|
||||
def _setup_ui(self, submit_text: str) -> None:
|
||||
# Root-контейнер документа: собирает все основные зоны окна сверху вниз.
|
||||
root = VContainer(margin=[24, 20, 24, 20], spacing=0)
|
||||
self.add_widget(root)
|
||||
root.add_widget(self._build_summary_shell())
|
||||
root.add_widget(self._build_divider())
|
||||
root.add_widget(self._build_form_shell())
|
||||
root.add_widget(self._build_actions_section(submit_text))
|
||||
|
||||
def _build_summary_shell(self) -> SContainer:
|
||||
# Summary-shell: верхняя область окна с базовой карточкой контекста документа.
|
||||
summary_shell = SContainer(
|
||||
height_percent=24,
|
||||
orientation="v",
|
||||
content_fit=False,
|
||||
)
|
||||
# Summary-content: вертикальный стек строк внутри summary-shell.
|
||||
summary_content = VContainer(spacing=10, content_fit=True, parent=summary_shell)
|
||||
for row in self._build_summary_rows():
|
||||
summary_content.add_widget(row)
|
||||
return summary_shell
|
||||
|
||||
def _build_summary_rows(self) -> list[HContainer]:
|
||||
institution, room, device = parse_location_parts(self._task.location)
|
||||
rows: list[HContainer] = []
|
||||
for title, value in (
|
||||
("Учреждение", institution or "Локация не указана"),
|
||||
("Оборудование", device or "Аппарат не указан"),
|
||||
("Кабинет", room or "Кабинет не указан"),
|
||||
("Специалист", self._build_specialist_summary()),
|
||||
):
|
||||
rows.append(self._build_summary_row(title, value))
|
||||
return rows
|
||||
|
||||
def _build_summary_row(self, title: str, value: str) -> HContainer:
|
||||
# Summary-row: горизонтальная строка для пары "заголовок поля + значение".
|
||||
row = HContainer(spacing=10, content_fit=True)
|
||||
title_label = Label(
|
||||
f"{title}:",
|
||||
alignment="left",
|
||||
style="TICKET_DETAILS_SUMMARY_TITLE",
|
||||
)
|
||||
value_label = Label(
|
||||
value,
|
||||
alignment="left",
|
||||
style="TICKET_DETAILS_SUMMARY_VALUE",
|
||||
)
|
||||
value_label.set_tooltip(value)
|
||||
row.add_widget(title_label)
|
||||
row.add_widget_with_stretch(value_label, 1)
|
||||
return row
|
||||
|
||||
def _build_divider(self) -> Label:
|
||||
return Label(
|
||||
"",
|
||||
style="TICKET_DETAILS_DIVIDER",
|
||||
height_percent=1,
|
||||
)
|
||||
|
||||
def _build_form_shell(self) -> SContainer:
|
||||
# Form-shell: центральная рабочая зона, куда наследник добавляет поля документа.
|
||||
form_shell = SContainer(
|
||||
height_percent=58,
|
||||
orientation="v",
|
||||
spacing=10,
|
||||
content_fit=False,
|
||||
)
|
||||
self._build_form(form_shell)
|
||||
return form_shell
|
||||
|
||||
def _build_actions_section(self, submit_text: str) -> HContainer:
|
||||
# Actions-row: нижняя линия действий с фиксированной процентной сеткой.
|
||||
actions = HContainer(
|
||||
height_percent=11,
|
||||
spacing=0,
|
||||
content_fit=False,
|
||||
)
|
||||
# Левый spacer-контейнер: формирует стартовый отступ перед кнопкой отмены.
|
||||
SContainer(
|
||||
width_percent=10,
|
||||
height_percent=100,
|
||||
parent=actions,
|
||||
)
|
||||
self._cancel_button = Button(
|
||||
"Отмена",
|
||||
width_percent=26,
|
||||
height_percent=100,
|
||||
margin=0,
|
||||
style="TICKET_DOCUMENT_CANCEL_BUTTON",
|
||||
content_fit=False,
|
||||
)
|
||||
actions.add_widget(self._cancel_button)
|
||||
# Центральный spacer-контейнер: удерживает зазор между двумя action-кнопками.
|
||||
SContainer(
|
||||
width_percent=4,
|
||||
height_percent=100,
|
||||
parent=actions,
|
||||
)
|
||||
self._submit_button = Button(
|
||||
submit_text,
|
||||
width_percent=50,
|
||||
height_percent=100,
|
||||
margin=0,
|
||||
style="TICKET_DOCUMENT_SUBMIT_BUTTON",
|
||||
content_fit=False,
|
||||
)
|
||||
actions.add_widget(self._submit_button)
|
||||
# Правый spacer-контейнер: завершает строку действий симметричным отступом.
|
||||
SContainer(
|
||||
width_percent=10,
|
||||
height_percent=100,
|
||||
parent=actions,
|
||||
)
|
||||
self._submit_button.set_enabled(False)
|
||||
return actions
|
||||
|
||||
def _connect_signals(self) -> None:
|
||||
if self._cancel_button is not None:
|
||||
self._cancel_button.clicked.connect(self.reject)
|
||||
if self._submit_button is not None:
|
||||
self._submit_button.clicked.connect(self.accept)
|
||||
for text_edit in self._form_text_edits:
|
||||
text_edit.text_changed.connect(self._refresh_submit_state)
|
||||
self._connect_form_signals()
|
||||
|
||||
def build_payload(self) -> dict[str, str]:
|
||||
"""Вернуть данные формы. Реализуется в наследниках."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _build_form(self, container: SContainer) -> None:
|
||||
"""Построить специфическую часть формы."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _is_ready(self) -> bool:
|
||||
"""Проверить, что форма готова к отправке."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _connect_form_signals(self) -> None:
|
||||
"""Подключить сигналы элементов формы, если кроме текстовых полей есть другие элементы."""
|
||||
return None
|
||||
|
||||
def _populate_text_block(
|
||||
self,
|
||||
field_shell: SContainer,
|
||||
title: str,
|
||||
placeholder: str,
|
||||
) -> TextInput:
|
||||
# Field-shell: ожидает внутри себя заголовок поля и текстовую область конкретной секции.
|
||||
field_shell.add_widget(
|
||||
Label(
|
||||
title,
|
||||
alignment="left",
|
||||
style="TICKET_DETAILS_SECTION_TITLE",
|
||||
)
|
||||
)
|
||||
text_edit = TextInput(
|
||||
placeholder=placeholder,
|
||||
style="TICKET_DOCUMENT_TEXTAREA",
|
||||
multiline=True,
|
||||
content_fit=False,
|
||||
)
|
||||
self._form_text_edits.append(text_edit)
|
||||
field_shell.add_widget_with_stretch(text_edit, 1)
|
||||
return text_edit
|
||||
|
||||
def _refresh_submit_state(self) -> None:
|
||||
if self._submit_button is not None:
|
||||
self._submit_button.set_enabled(self._is_ready())
|
||||
|
||||
def _build_specialist_summary(self) -> str:
|
||||
specialist_name = self._task.assigned_specialist.strip()
|
||||
if not specialist_name:
|
||||
return "Не назначен"
|
||||
specialist_info = build_specialist_card_info(specialist_name)
|
||||
short_name = specialist_info["short_name"].strip() or specialist_name
|
||||
position = specialist_info["position"].strip()
|
||||
if not position:
|
||||
return short_name
|
||||
return f"{short_name}, {position}"
|
||||
11
Dispatch_V0.1.1/ui/dialogs/report_dialogs/__init__.py
Normal file
11
Dispatch_V0.1.1/ui/dialogs/report_dialogs/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/report_dialogs/__init__.py
|
||||
|
||||
"""Диалоги отчётов Ticket."""
|
||||
|
||||
from .report_dialog import DiagnosticReportDialog, RepairReportDialog
|
||||
|
||||
__all__ = [
|
||||
"DiagnosticReportDialog",
|
||||
"RepairReportDialog",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
137
Dispatch_V0.1.1/ui/dialogs/report_dialogs/report_dialog.py
Normal file
137
Dispatch_V0.1.1/ui/dialogs/report_dialogs/report_dialog.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/report_dialogs/report_dialog.py
|
||||
|
||||
"""UI-диалоги отчётов Ticket поверх application document-flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gui.containers import SContainer
|
||||
|
||||
from domain import TicketTaskSnapshot
|
||||
from ui.dialogs.base_document_dialog import BaseDocumentDialog
|
||||
|
||||
|
||||
class DiagnosticReportDialog(BaseDocumentDialog):
|
||||
"""Диалог диагностического отчёта."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
):
|
||||
self._initial_cause = None
|
||||
self._actual_cause = None
|
||||
super().__init__(
|
||||
task=task,
|
||||
title="Диагностический отчёт",
|
||||
submit_text="Подписать диагностику",
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
def build_payload(self) -> dict[str, str]:
|
||||
return {
|
||||
"initial_cause": self._initial_cause.get_text().strip(),
|
||||
"actual_cause": self._actual_cause.get_text().strip(),
|
||||
}
|
||||
|
||||
def _is_ready(self) -> bool:
|
||||
payload = self.build_payload()
|
||||
return bool(payload["initial_cause"] and payload["actual_cause"])
|
||||
|
||||
def _build_form(self, container: SContainer) -> None:
|
||||
initial_cause_shell = SContainer(
|
||||
height_percent=48,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._initial_cause = self._populate_text_block(
|
||||
initial_cause_shell,
|
||||
"Первичное заключение",
|
||||
"Кратко опишите исходную причину неисправности.",
|
||||
)
|
||||
container.add_widget(initial_cause_shell)
|
||||
|
||||
actual_cause_shell = SContainer(
|
||||
height_percent=48,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._actual_cause = self._populate_text_block(
|
||||
actual_cause_shell,
|
||||
"Вторичное заключение",
|
||||
"Зафиксируйте подтверждённую причину по итогам диагностики.",
|
||||
)
|
||||
container.add_widget(actual_cause_shell)
|
||||
self._refresh_submit_state()
|
||||
|
||||
|
||||
class RepairReportDialog(BaseDocumentDialog):
|
||||
"""Диалог ремонтного отчёта."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
parent=None,
|
||||
):
|
||||
self._work_done = None
|
||||
self._used_parts = None
|
||||
self._recommendations = None
|
||||
super().__init__(
|
||||
task=task,
|
||||
title="Ремонтный отчёт",
|
||||
submit_text="Подписать ремонт",
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
def build_payload(self) -> dict[str, str]:
|
||||
return {
|
||||
"work_done": self._work_done.get_text().strip(),
|
||||
"used_parts": self._used_parts.get_text().strip(),
|
||||
"recommendations": self._recommendations.get_text().strip(),
|
||||
}
|
||||
|
||||
def _is_ready(self) -> bool:
|
||||
return bool(self.build_payload()["work_done"])
|
||||
|
||||
def _build_form(self, container: SContainer) -> None:
|
||||
work_done_shell = SContainer(
|
||||
height_percent=31,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._work_done = self._populate_text_block(
|
||||
work_done_shell,
|
||||
"Выполненные работы",
|
||||
"Опишите фактически выполненные работы.",
|
||||
)
|
||||
container.add_widget(work_done_shell)
|
||||
|
||||
used_parts_shell = SContainer(
|
||||
height_percent=31,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._used_parts = self._populate_text_block(
|
||||
used_parts_shell,
|
||||
"Использованные запчасти",
|
||||
"Перечислите использованные узлы и материалы, если они были.",
|
||||
)
|
||||
container.add_widget(used_parts_shell)
|
||||
|
||||
recommendations_shell = SContainer(
|
||||
height_percent=31,
|
||||
orientation="v",
|
||||
spacing=6,
|
||||
content_fit=False,
|
||||
)
|
||||
self._recommendations = self._populate_text_block(
|
||||
recommendations_shell,
|
||||
"Рекомендации",
|
||||
"Добавьте рекомендации для следующего обслуживания.",
|
||||
)
|
||||
container.add_widget(recommendations_shell)
|
||||
self._refresh_submit_state()
|
||||
223
Dispatch_V0.1.1/ui/dialogs/specialist_dialog.py
Normal file
223
Dispatch_V0.1.1/ui/dialogs/specialist_dialog.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/specialist_dialog.py
|
||||
|
||||
"""UI-диалог выбора специалиста Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QMouseEvent
|
||||
|
||||
from gui.components import Button, Dialog, Label
|
||||
from gui.containers import HContainer, ScrollContainer, SContainer, VContainer
|
||||
|
||||
from ui.cards.task_card_pixmap_factory import (
|
||||
build_placeholder_avatar_pixmap,
|
||||
load_avatar_pixmap,
|
||||
)
|
||||
from ui.task_view_formatters import (
|
||||
build_specialist_card_info,
|
||||
build_specialist_photo_path,
|
||||
)
|
||||
|
||||
|
||||
class _SpecialistRow(SContainer):
|
||||
"""Строка выбора специалиста с фото, именем и должностью."""
|
||||
|
||||
clicked = Signal(str)
|
||||
activated = Signal(str)
|
||||
|
||||
def __init__(self, specialist_name: str, parent=None):
|
||||
super().__init__(
|
||||
margin=0,
|
||||
spacing=0,
|
||||
content_fit=True,
|
||||
parent=parent,
|
||||
style="TICKET_SPECIALIST_ITEM",
|
||||
active_style="TICKET_SPECIALIST_ITEM_SELECTED",
|
||||
is_active=False,
|
||||
)
|
||||
self._specialist_name = specialist_name
|
||||
self._info = build_specialist_card_info(specialist_name)
|
||||
self._name_label: Label | None = None
|
||||
self._role_label: Label | None = None
|
||||
self._avatar_label: Label | None = None
|
||||
self.setObjectName("ticket_specialist_row")
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.set_min_height(88)
|
||||
self._setup_ui()
|
||||
|
||||
@property
|
||||
def specialist_name(self) -> str:
|
||||
return self._specialist_name
|
||||
|
||||
def set_selected(self, selected: bool) -> None:
|
||||
self.style(is_active=selected)
|
||||
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit(self._specialist_name)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QMouseEvent) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit(self._specialist_name)
|
||||
self.activated.emit(self._specialist_name)
|
||||
super().mouseDoubleClickEvent(event)
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
# Body-row карточки специалиста: фото слева и текстовый блок справа.
|
||||
body = HContainer(margin=[12, 10, 12, 10], spacing=16, content_fit=True, parent=self)
|
||||
body.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
|
||||
self._avatar_label = Label("", style="TICKET_TASK_CARD_AVATAR_IMAGE")
|
||||
self._avatar_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
self._avatar_label.set_fixed_size(72, 72)
|
||||
self._avatar_label.set_pixmap(self._build_avatar_pixmap())
|
||||
|
||||
self._name_label = Label(
|
||||
self._specialist_name,
|
||||
alignment="left",
|
||||
style="TICKET_DETAILS_SECTION_TITLE",
|
||||
)
|
||||
self._name_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
|
||||
self._role_label = Label(
|
||||
self._info.get("position", "").strip() or "Специалист",
|
||||
alignment="left",
|
||||
style="TICKET_SPECIALIST_ROLE",
|
||||
)
|
||||
self._role_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
|
||||
# Text-block специалиста: вертикальный контейнер имени и должности.
|
||||
text_block = VContainer(spacing=2, content_fit=True)
|
||||
text_block.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
|
||||
text_block.add_widget(self._name_label)
|
||||
text_block.add_widget(self._role_label)
|
||||
|
||||
body.add_widget(self._avatar_label)
|
||||
body.add_widget_with_stretch(text_block, 1)
|
||||
|
||||
def _build_avatar_pixmap(self):
|
||||
photo_path = build_specialist_photo_path(
|
||||
self._specialist_name,
|
||||
self._info.get("photo", ""),
|
||||
)
|
||||
avatar = load_avatar_pixmap(photo_path, 72, 72, padding=2)
|
||||
if avatar is None:
|
||||
return build_placeholder_avatar_pixmap(72)
|
||||
return avatar
|
||||
|
||||
|
||||
class SpecialistDialog(Dialog):
|
||||
"""Диалог выбора специалиста без application-логики."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
specialists: Sequence[str],
|
||||
parent=None,
|
||||
):
|
||||
self._specialists = [str(item).strip() for item in specialists if str(item).strip()]
|
||||
self._selected_specialist = ""
|
||||
self._rows: dict[str, _SpecialistRow] = {}
|
||||
self._cancel_button: Button | None = None
|
||||
self._submit_button: Button | None = None
|
||||
super().__init__(
|
||||
title="Выбор специалиста",
|
||||
width=540,
|
||||
height=740,
|
||||
modal=True,
|
||||
parent=parent,
|
||||
)
|
||||
self._setup_ui()
|
||||
self._connect_signals()
|
||||
self._refresh_submit_state()
|
||||
|
||||
@property
|
||||
def selected_specialist(self) -> str:
|
||||
return self._selected_specialist
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
# Root-контейнер диалога: подсказка, список специалистов и action-строка.
|
||||
main_container = VContainer(margin=[22, 20, 22, 20], spacing=16)
|
||||
self.add_widget(main_container)
|
||||
main_container.add_widget(
|
||||
Label(
|
||||
"Выберите специалиста для назначения на\nзадачу:",
|
||||
alignment="left",
|
||||
style="TICKET_SPECIALIST_HINT",
|
||||
)
|
||||
)
|
||||
main_container.add_widget_with_stretch(self._build_list(), 1)
|
||||
main_container.add_widget(self._build_actions())
|
||||
|
||||
def _build_list(self) -> ScrollContainer:
|
||||
# Scroll-list специалистов: прокручиваемая область с вариантами назначения.
|
||||
scroll = ScrollContainer(
|
||||
spacing=12,
|
||||
orientation="v",
|
||||
content_margins=[0, 0, 0, 0],
|
||||
vertical_scroll_bar_policy="as_needed",
|
||||
horizontal_scroll_bar_policy="always_off",
|
||||
style="SCROLL_CONTAINER",
|
||||
)
|
||||
for specialist_name in self._specialists:
|
||||
row = _SpecialistRow(specialist_name)
|
||||
self._rows[specialist_name] = row
|
||||
scroll.add_widget(row)
|
||||
return scroll
|
||||
|
||||
def _build_actions(self) -> HContainer:
|
||||
# Actions-row диалога: выравнивает кнопки отмены и подтверждения выбора.
|
||||
actions = HContainer(spacing=18, content_fit=True)
|
||||
actions.add_stretch()
|
||||
self._cancel_button = Button(
|
||||
"Отмена",
|
||||
style="TICKET_DOCUMENT_CANCEL_BUTTON",
|
||||
content_fit=True,
|
||||
)
|
||||
self._cancel_button.set_min_width(160)
|
||||
self._cancel_button.set_min_height(56)
|
||||
self._submit_button = Button(
|
||||
"Выбрать",
|
||||
style="TICKET_DOCUMENT_SUBMIT_BUTTON",
|
||||
content_fit=True,
|
||||
)
|
||||
self._submit_button.set_min_width(180)
|
||||
self._submit_button.set_min_height(56)
|
||||
actions.add_widget(self._cancel_button)
|
||||
actions.add_widget(self._submit_button)
|
||||
return actions
|
||||
|
||||
def _connect_signals(self) -> None:
|
||||
for row in self._rows.values():
|
||||
row.clicked.connect(self._on_row_clicked)
|
||||
row.activated.connect(self._on_row_activated)
|
||||
if self._submit_button is not None:
|
||||
self._submit_button.clicked.connect(self._handle_accept)
|
||||
if self._cancel_button is not None:
|
||||
self._cancel_button.clicked.connect(self.reject)
|
||||
|
||||
def _refresh_submit_state(self) -> None:
|
||||
if self._submit_button is not None:
|
||||
self._submit_button.set_enabled(bool(self._selected_specialist))
|
||||
|
||||
def _set_selected_specialist(self, specialist_name: str) -> None:
|
||||
self._selected_specialist = specialist_name if specialist_name in self._rows else ""
|
||||
for row_name, row in self._rows.items():
|
||||
row.set_selected(row_name == self._selected_specialist)
|
||||
self._refresh_submit_state()
|
||||
|
||||
def _handle_accept(self) -> None:
|
||||
if not self._selected_specialist:
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def _on_row_clicked(self, specialist_name: str) -> None:
|
||||
self._set_selected_specialist(specialist_name)
|
||||
|
||||
def _on_row_activated(self, specialist_name: str) -> None:
|
||||
self._set_selected_specialist(specialist_name)
|
||||
self._handle_accept()
|
||||
135
Dispatch_V0.1.1/ui/dialogs/task_refusal_dialog.py
Normal file
135
Dispatch_V0.1.1/ui/dialogs/task_refusal_dialog.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/ui/dialogs/task_refusal_dialog.py
|
||||
|
||||
"""Диалог подтверждения отказа задачи Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from gui.components import Button, Dialog, Label, TextInput
|
||||
from gui.containers import HContainer, VContainer
|
||||
|
||||
from domain import TicketTaskSnapshot, parse_location_parts
|
||||
|
||||
|
||||
class TaskRefusalDialog(Dialog):
|
||||
"""Диалог ввода обязательной причины отказа по задаче."""
|
||||
|
||||
def __init__(self, task: TicketTaskSnapshot, parent=None):
|
||||
self._task = task
|
||||
self._reason_input: TextInput | None = None
|
||||
self._cancel_button: Button | None = None
|
||||
self._submit_button: Button | None = None
|
||||
super().__init__(
|
||||
title="Отказ в обслуживании",
|
||||
width=500,
|
||||
height=460,
|
||||
modal=True,
|
||||
parent=parent,
|
||||
)
|
||||
self._setup_ui()
|
||||
self._connect_signals()
|
||||
self._refresh_submit_state()
|
||||
|
||||
@property
|
||||
def refusal_reason(self) -> str:
|
||||
if self._reason_input is None:
|
||||
return ""
|
||||
return self._reason_input.get_text().strip()
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
# Root-контейнер окна отказа: предупреждение, контекст задачи, поле причины и actions.
|
||||
main_container = VContainer(margin=[24, 20, 24, 20], spacing=16)
|
||||
self.add_widget(main_container)
|
||||
main_container.add_widget(
|
||||
Label(
|
||||
"Вы уверены, что хотите отказать в обслуживании?",
|
||||
alignment="left",
|
||||
style="TICKET_REFUSAL_HEADING",
|
||||
)
|
||||
)
|
||||
main_container.add_widget(self._build_location_row())
|
||||
main_container.add_widget(
|
||||
Label(
|
||||
'Задача будет перемещена в колонку "Отказ"',
|
||||
alignment="left",
|
||||
style="TICKET_REFUSAL_WARNING",
|
||||
)
|
||||
)
|
||||
main_container.add_widget(
|
||||
Label(
|
||||
"Причина отказа",
|
||||
alignment="left",
|
||||
style="TICKET_REFUSAL_HEADING",
|
||||
)
|
||||
)
|
||||
main_container.add_widget_with_stretch(self._build_reason_input(), 1)
|
||||
main_container.add_widget(self._build_actions())
|
||||
|
||||
def _build_location_row(self) -> HContainer:
|
||||
# Location-row: показывает локацию задачи, чтобы отказ происходил в явном контексте.
|
||||
row = HContainer(spacing=10, content_fit=True)
|
||||
row.add_widget(
|
||||
Label(
|
||||
"Локация и кабинет:",
|
||||
alignment="left",
|
||||
style="TICKET_REFUSAL_LOCATION_TITLE",
|
||||
)
|
||||
)
|
||||
value_label = Label(
|
||||
self._build_location_text(),
|
||||
alignment="left",
|
||||
style="TICKET_REFUSAL_LOCATION_VALUE",
|
||||
)
|
||||
value_label.set_tooltip(self._build_location_text())
|
||||
row.add_widget_with_stretch(value_label, 1)
|
||||
return row
|
||||
|
||||
def _build_reason_input(self) -> TextInput:
|
||||
self._reason_input = TextInput(
|
||||
placeholder="Укажите причину отказа",
|
||||
style="TICKET_DOCUMENT_TEXTAREA",
|
||||
multiline=True,
|
||||
)
|
||||
self._reason_input.set_min_height(170)
|
||||
return self._reason_input
|
||||
|
||||
def _build_actions(self) -> HContainer:
|
||||
# Actions-row окна отказа: собирает кнопки отмены и финального подтверждения.
|
||||
actions = HContainer(spacing=18, content_fit=True)
|
||||
actions.add_stretch()
|
||||
self._cancel_button = Button(
|
||||
"Отмена",
|
||||
style="TICKET_DOCUMENT_CANCEL_BUTTON",
|
||||
content_fit=True,
|
||||
)
|
||||
self._submit_button = Button(
|
||||
"Подтвердить отказ",
|
||||
style="TICKET_DOCUMENT_SUBMIT_BUTTON",
|
||||
content_fit=True,
|
||||
)
|
||||
actions.add_widget(self._cancel_button)
|
||||
actions.add_widget(self._submit_button)
|
||||
return actions
|
||||
|
||||
def _connect_signals(self) -> None:
|
||||
if self._reason_input is not None:
|
||||
self._reason_input.text_changed.connect(self._refresh_submit_state)
|
||||
if self._cancel_button is not None:
|
||||
self._cancel_button.clicked.connect(self.reject)
|
||||
if self._submit_button is not None:
|
||||
self._submit_button.clicked.connect(self._handle_accept)
|
||||
|
||||
def _refresh_submit_state(self) -> None:
|
||||
if self._submit_button is not None:
|
||||
self._submit_button.set_enabled(bool(self.refusal_reason))
|
||||
|
||||
def _handle_accept(self) -> None:
|
||||
if not self.refusal_reason:
|
||||
return
|
||||
self.accept()
|
||||
|
||||
def _build_location_text(self) -> str:
|
||||
institution, room, _ = parse_location_parts(self._task.location or "")
|
||||
normalized_institution = institution or "Локация не указана"
|
||||
normalized_room = room or "Кабинет не указан"
|
||||
return f"{normalized_institution}, {normalized_room}"
|
||||
Reference in New Issue
Block a user