377 lines
12 KiB
Python
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()
|
|
|
|
|