handOver2/ui/widgets/daily_inspection.py

377 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""
일상검수 위젯 모듈
일상검수 대상 편성을 표시하고 관리하는 위젯입니다.
"""
from datetime import date
from typing import List, Optional
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QPushButton, QFrame, QDialog
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QColor, QPainter, QPen
from ui.base.base_widget import BaseWidget, CardWidget
from ui.components.custom_button import CustomButton
from ui.dialogs.train_input_dialog import TrainInputDialog
from database.crud import CRUDManager
from database.models import DailyInspection
from core.constants import DAILY_INSPECTION_SLOTS, CLEANING_TYPES, SHIFT_TYPES
from core.config import ConfigManager
from core.signals import GlobalSignals
from core.logger import get_logger
logger = get_logger(__name__)
class TrainSlot(QPushButton):
"""
편성 슬롯 위젯
일상검수 편성 하나를 표시하는 슬롯입니다.
청소 유형에 따라 다른 표시를 합니다.
"""
slot_clicked = Signal(int) # 슬롯 번호
def __init__(
self,
slot_number: int,
parent=None,
train_number: str = "",
cleaning_type: str = "없음",
has_work: bool = False
):
super().__init__(parent)
self.config = ConfigManager()
self.slot_number = slot_number
self._train_number = train_number
self._cleaning_type = cleaning_type
self._has_work = has_work
# 기본 설정 - 151A 형식 표시를 위해 크기 조정
self.setFixedSize(70, 50)
self.setCursor(Qt.PointingHandCursor)
self.setFont(QFont("GmarketSans", 12, QFont.Bold))
self.clicked.connect(lambda: self.slot_clicked.emit(self.slot_number))
self._update_display()
def _update_display(self):
"""표시 업데이트"""
if self._train_number:
self.setText(self._train_number)
else:
self.setText("+")
self._apply_style()
def _apply_style(self):
"""스타일 적용"""
theme = self.config.theme
if theme == 'dark':
bg = "#334155"
bg_hover = "#475569"
text = "#f8fafc"
empty_bg = "#1e293b"
empty_text = "#64748b"
else:
bg = "#e2e8f0"
bg_hover = "#cbd5e1"
text = "#1e293b"
empty_bg = "#f1f5f9"
empty_text = "#94a3b8"
if self._train_number:
self.setStyleSheet(f"""
QPushButton {{
background-color: {bg};
color: {text};
border: none;
border-radius: 8px;
font-weight: bold;
}}
QPushButton:hover {{
background-color: {bg_hover};
}}
""")
else:
self.setStyleSheet(f"""
QPushButton {{
background-color: {empty_bg};
color: {empty_text};
border: 2px dashed {empty_text};
border-radius: 8px;
font-size: 20px;
}}
QPushButton:hover {{
border-color: {text};
color: {text};
}}
""")
def paintEvent(self, event):
"""페인트 이벤트 (청소/작업 표시)"""
super().paintEvent(event)
if not self._train_number:
return
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 청소 표시
if self._cleaning_type == "중청소":
# 파란 네모
painter.setPen(QPen(QColor("#3b82f6"), 2))
painter.setBrush(Qt.NoBrush)
painter.drawRect(4, 4, 14, 14)
elif self._cleaning_type == "대청소":
# 빨간 동그라미
painter.setPen(QPen(QColor("#ef4444"), 2))
painter.setBrush(Qt.NoBrush)
painter.drawEllipse(4, 4, 14, 14)
# 작업 표시 (노란 느낌표)
if self._has_work:
painter.setPen(QPen(QColor("#f59e0b"), 2))
painter.setFont(QFont("Arial", 10, QFont.Bold))
painter.drawText(self.width() - 14, 14, "!")
def set_data(self, train_number: str, cleaning_type: str, has_work: bool):
"""데이터 설정"""
self._train_number = train_number
self._cleaning_type = cleaning_type
self._has_work = has_work
self._update_display()
self.update()
def clear(self):
"""슬롯 비우기"""
self._train_number = ""
self._cleaning_type = "없음"
self._has_work = False
self._update_display()
self.update()
class ShiftInspectionRow(QWidget):
"""
근무 유형별 일상검수 행
주간/야간 각각의 5개 슬롯을 표시합니다.
"""
slot_clicked = Signal(str, int) # 근무유형, 슬롯번호
def __init__(self, shift_type: str, parent=None):
super().__init__(parent)
self.config = ConfigManager()
self.shift_type = shift_type
self.slots: List[TrainSlot] = []
self._setup_ui()
def _setup_ui(self):
"""UI 설정"""
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
# 근무 유형 라벨
shift_label = QLabel(self.shift_type)
shift_label.setFixedWidth(40)
shift_label.setFont(QFont("GmarketSans", 11, QFont.Bold))
shift_label.setAlignment(Qt.AlignCenter)
theme = self.config.theme
text_color = "#f8fafc" if theme == 'dark' else "#1e293b"
shift_label.setStyleSheet(f"color: {text_color};")
layout.addWidget(shift_label)
# 슬롯들
for i in range(DAILY_INSPECTION_SLOTS):
slot = TrainSlot(i + 1)
slot.slot_clicked.connect(
lambda num, st=self.shift_type: self.slot_clicked.emit(st, num)
)
self.slots.append(slot)
layout.addWidget(slot)
layout.addStretch()
def set_slot_data(self, slot_number: int, train_number: str, cleaning_type: str, has_work: bool):
"""슬롯 데이터 설정"""
if 1 <= slot_number <= len(self.slots):
self.slots[slot_number - 1].set_data(train_number, cleaning_type, has_work)
def clear_all(self):
"""모든 슬롯 비우기"""
for slot in self.slots:
slot.clear()
class DailyInspectionWidget(CardWidget):
"""
일상검수 위젯
주간/야간 일상검수 편성을 표시하고 관리합니다.
Examples:
>>> widget = DailyInspectionWidget()
>>> widget.load_data()
"""
def __init__(self, parent=None):
super().__init__(parent, padding=12, radius=12)
self.crud = CRUDManager()
self._current_date = date.today()
self._setup_ui()
self._connect_signals()
logger.info("일상검수 위젯 초기화 완료")
def _setup_ui(self):
"""UI 설정"""
# 헤더
header = QWidget()
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(0, 0, 0, 0)
title = QLabel("📋 일상검수")
title.setFont(QFont("GmarketSans", 14, QFont.Bold))
theme = self.config.theme
text_color = "#f8fafc" if theme == 'dark' else "#1e293b"
title.setStyleSheet(f"color: {text_color};")
header_layout.addWidget(title)
header_layout.addStretch()
self.layout.addWidget(header)
# 구분선
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setStyleSheet("color: #334155;" if theme == 'dark' else "color: #e2e8f0;")
self.layout.addWidget(separator)
# 주간/야간 행
self.day_row = ShiftInspectionRow("주간")
self.day_row.slot_clicked.connect(self._on_slot_clicked)
self.layout.addWidget(self.day_row)
self.night_row = ShiftInspectionRow("야간")
self.night_row.slot_clicked.connect(self._on_slot_clicked)
self.layout.addWidget(self.night_row)
# 범례
self._create_legend()
def _create_legend(self):
"""범례 생성"""
legend = QWidget()
legend_layout = QHBoxLayout(legend)
legend_layout.setContentsMargins(0, 8, 0, 0)
legend_layout.setSpacing(16)
theme = self.config.theme
text_color = "#94a3b8" if theme == 'dark' else "#64748b"
items = [
("", "#3b82f6", "중청소"),
("", "#ef4444", "대청소"),
("!", "#f59e0b", "작업"),
]
for symbol, color, label in items:
item = QLabel(f'<span style="color:{color}; font-weight:bold;">{symbol}</span> {label}')
item.setFont(QFont("GmarketSans", 10))
item.setStyleSheet(f"color: {text_color};")
legend_layout.addWidget(item)
legend_layout.addStretch()
self.layout.addWidget(legend)
def _connect_signals(self):
"""시그널 연결"""
self.signals.daily_inspection_changed.connect(self._on_inspection_changed)
def load_data(self):
"""데이터 로드"""
# 주간 데이터
day_inspections = self.crud.get_daily_inspections_by_date(
self._current_date, "주간"
)
self.day_row.clear_all()
for inspection in day_inspections:
self.day_row.set_slot_data(
inspection.slot_number,
inspection.train_number or "",
inspection.cleaning_type or "없음",
inspection.has_work
)
# 야간 데이터
night_inspections = self.crud.get_daily_inspections_by_date(
self._current_date, "야간"
)
self.night_row.clear_all()
for inspection in night_inspections:
self.night_row.set_slot_data(
inspection.slot_number,
inspection.train_number or "",
inspection.cleaning_type or "없음",
inspection.has_work
)
def _on_slot_clicked(self, shift_type: str, slot_number: int):
"""슬롯 클릭"""
# 현재 등록된 모든 편성 조회 (중복 방지용)
existing_trains = []
day_inspections = self.crud.get_daily_inspections_by_date(self._current_date, "주간")
night_inspections = self.crud.get_daily_inspections_by_date(self._current_date, "야간")
for insp in day_inspections + night_inspections:
if insp.train_number:
existing_trains.append(insp.train_number)
dialog = TrainInputDialog(
self,
shift_type,
slot_number,
self._current_date,
existing_trains=existing_trains
)
if dialog.exec() == QDialog.Accepted:
data = dialog.get_data()
self.crud.upsert_daily_inspection(
inspection_date=self._current_date,
shift_type=shift_type,
slot_number=slot_number,
train_number=data.get("train_number", ""),
cleaning_type=data.get("cleaning_type", "없음"),
has_work=data.get("has_work", False),
work_content=data.get("work_content", ""),
is_work_completed=data.get("is_work_completed", False),
)
self.load_data()
self.signals.daily_inspection_changed.emit(shift_type, slot_number, data.get("train_number", ""))
def _on_inspection_changed(self, shift_type: str, slot_number: int, train_number: str):
"""일상검수 변경 시그널"""
self.load_data()