""" 연차 상세 내역 뷰 """ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QGroupBox, QMessageBox, QInputDialog, QLineEdit, QDateEdit, QComboBox, QDoubleSpinBox, QMenu, QAction) from PyQt5.QtCore import Qt, QDate from PyQt5.QtGui import QColor from datetime import datetime import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from core.database import Database from core.i18n import tr from ui.styles import apply_dark_titlebar class LeaveView(QDialog): """연차 상세 내역 다이얼로그""" def __init__(self, parent=None, db=None): super().__init__(parent) self.db = db if db else Database() self.init_ui() self.load_data() apply_dark_titlebar(self) def init_ui(self): """UI 초기화""" self.setWindowTitle(tr('window.leave_view')) self.setModal(True) self.setMinimumSize(700, 450) layout = QVBoxLayout() layout.setSpacing(6) layout.setContentsMargins(12, 10, 12, 10) # 제목 + 잔액 + 설정 한 줄 header_layout = QHBoxLayout() title = QLabel(tr('view.leave.title')) title.setObjectName("dialog_title") header_layout.addWidget(title) header_layout.addStretch() self.balance_label = QLabel(tr('view.leave.balance_zero')) self.balance_label.setObjectName("badge_leave") header_layout.addWidget(self.balance_label) set_balance_button = QPushButton(tr('view.leave.btn_set_balance')) set_balance_button.clicked.connect(self.set_balance) header_layout.addWidget(set_balance_button) layout.addLayout(header_layout) # 사용 내역 used_group = QGroupBox(tr('view.leave.used_group')) used_layout = QVBoxLayout() used_layout.setSpacing(4) used_layout.setContentsMargins(8, 20, 8, 6) self.used_table = QTableWidget() self.used_table.setColumnCount(4) self.used_table.setHorizontalHeaderLabels([ tr('view.leave.col_date'), tr('view.leave.col_type'), tr('view.leave.col_used'), tr('view.leave.col_reason'), ]) self.used_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents) self.used_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.used_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) self.used_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Stretch) self.used_table.setAlternatingRowColors(True) self.used_table.setEditTriggers(QTableWidget.NoEditTriggers) self.used_table.setSelectionBehavior(QTableWidget.SelectRows) self.used_table.setContextMenuPolicy(Qt.CustomContextMenu) self.used_table.customContextMenuRequested.connect(self.show_context_menu) used_layout.addWidget(self.used_table) used_group.setLayout(used_layout) layout.addWidget(used_group) # 버튼들 button_layout = QHBoxLayout() add_leave_button = QPushButton(tr('view.leave.btn_add')) add_leave_button.clicked.connect(self.add_leave_record) button_layout.addWidget(add_leave_button) cal_button = QPushButton(tr('view.leave.btn_calendar')) cal_button.clicked.connect(self._show_calendar) button_layout.addWidget(cal_button) schedule_button = QPushButton("스케줄") schedule_button.setToolTip("휴일 + 연차 + 반복 패턴 통합 보기") schedule_button.clicked.connect(self._show_schedule) button_layout.addWidget(schedule_button) close_button = QPushButton(tr('btn.close')) close_button.clicked.connect(self.close) button_layout.addWidget(close_button) layout.addLayout(button_layout) self.setLayout(layout) def _show_calendar(self): from ui.leave_calendar_view import LeaveCalendarView dlg = LeaveCalendarView(self, self.db) dlg.exec_() def _show_schedule(self): from ui.schedule_view import ScheduleView dlg = ScheduleView(self, self.db) dlg.exec_() # 닫고 돌아오면 잔액/리스트 갱신 self.load_data() def load_data(self): """데이터 로드""" # 잔액 업데이트 balance = self.db.get_leave_balance() hours = balance * 8 self.balance_label.setText(tr('view.leave.balance_fmt', days=balance, hours=hours)) # 사용 내역 로드 (잔액 조정 제외) records = self.db.get_leave_records(exclude_bulk=True) self.used_table.setRowCount(len(records)) for i, record in enumerate(records): date_item = QTableWidgetItem(record['date']) date_item.setTextAlignment(Qt.AlignCenter) date_item.setData(Qt.UserRole, record['id']) type_item = QTableWidgetItem(record['leave_type']) type_item.setTextAlignment(Qt.AlignCenter) days = record['days'] hours = days * 8 if days == 1.0: days_str = "1일" elif days == 0.5: days_str = "0.5일 (4시간)" elif hours < 8: days_str = f"{days}일 ({hours}시간)" else: days_str = f"{days}일" days_item = QTableWidgetItem(days_str) days_item.setTextAlignment(Qt.AlignCenter) days_item.setForeground(QColor(250, 82, 82)) # 사용 = 레드 (#FA5252) memo_item = QTableWidgetItem(record['memo'] or "") self.used_table.setItem(i, 0, date_item) self.used_table.setItem(i, 1, type_item) self.used_table.setItem(i, 2, days_item) self.used_table.setItem(i, 3, memo_item) def show_context_menu(self, position): """사용 내역 우클릭 메뉴""" selected_rows = self.used_table.selectionModel().selectedRows() if not selected_rows: return menu = QMenu(self) delete_action = QAction(tr('btn.delete_short'), self) delete_action.triggered.connect(self.delete_leave_record) menu.addAction(delete_action) menu.exec_(self.used_table.viewport().mapToGlobal(position)) def delete_leave_record(self): """연차 사용 기록 삭제""" selected_rows = self.used_table.selectionModel().selectedRows() if not selected_rows: return row = selected_rows[0].row() date_item = self.used_table.item(row, 0) type_item = self.used_table.item(row, 1) days_item = self.used_table.item(row, 2) leave_id = date_item.data(Qt.UserRole) reply = QMessageBox.question( self, tr('msg.confirm_delete.title'), tr('view.leave.delete_confirm_body', date=date_item.text(), type=type_item.text(), days=days_item.text()), QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.db.delete_leave_record(leave_id) self.load_data() def set_balance(self): """연차 개수 설정 (시간 단위)""" current_balance = self.db.get_leave_balance() current_hours = current_balance * 8 hours, ok = QInputDialog.getDouble( self, tr('view.leave.set_title'), tr('view.leave.set_prompt'), current_hours, 0.0, 999.0, 1 ) if ok: # 0.5시간 단위로 반올림 hours = round(hours * 2) / 2 # 시간을 일수로 변환 days = hours / 8.0 self.db.set_leave_balance(days) QMessageBox.information( self, tr('view.leave.set_done_title'), tr('view.leave.set_done_body', days=days, hours=hours) ) self.load_data() def add_leave_record(self): """연차 사용 기록 추가 다이얼로그""" dialog = AddLeaveDialog(self, self.db) if dialog.exec_() == QDialog.Accepted: self.load_data() class AddLeaveDialog(QDialog): """연차 사용 기록 추가 다이얼로그""" def __init__(self, parent=None, db=None): super().__init__(parent) self.db = db self.init_ui() apply_dark_titlebar(self) def init_ui(self): """UI 초기화""" self.setWindowTitle(tr('view.leave.add_title')) self.setModal(True) self.setMinimumWidth(360) layout = QVBoxLayout() layout.setSpacing(8) layout.setContentsMargins(12, 10, 12, 10) # 제목 title = QLabel(tr('view.leave.add_title')) title.setObjectName("dialog_subtitle") title.setAlignment(Qt.AlignCenter) layout.addWidget(title) # 날짜 + 구분 한 줄 row1 = QHBoxLayout() date_label = QLabel(tr('view.leave.field_date')) date_label.setObjectName("field_label") date_label.setFixedWidth(40) self.date_edit = QDateEdit() self.date_edit.setDate(QDate.currentDate()) self.date_edit.setCalendarPopup(True) # 미래 1년까지 등록 가능 (Phase 1: 미리 등록) self.date_edit.setMaximumDate(QDate.currentDate().addYears(1)) row1.addWidget(date_label) row1.addWidget(self.date_edit) row1.addSpacing(8) type_label = QLabel(tr('view.leave.field_type')) type_label.setObjectName("field_label") type_label.setFixedWidth(40) self.type_combo = QComboBox() self.type_combo.addItem(tr('view.leave.type_annual'), "annual") self.type_combo.addItem(tr('view.leave.type_half'), "half") self.type_combo.addItem(tr('view.leave.type_quarter'), "quarter") self.type_combo.addItem(tr('view.leave.type_hourly'), "hourly") self.type_combo.currentIndexChanged.connect(self.on_type_changed) row1.addWidget(type_label) row1.addWidget(self.type_combo) layout.addLayout(row1) # 사용 시간 (시간 연차용) hours_layout = QHBoxLayout() hours_label = QLabel(tr('view.leave.field_hours')) hours_label.setObjectName("field_label") hours_label.setFixedWidth(40) self.hours_spin = QDoubleSpinBox() self.hours_spin.setRange(0.5, 8.0) self.hours_spin.setSingleStep(0.5) self.hours_spin.setValue(1.0) self.hours_spin.setSuffix(' ' + tr('label.unit_hour')) self.hours_spin.setEnabled(False) hours_layout.addWidget(hours_label) hours_layout.addWidget(self.hours_spin) layout.addLayout(hours_layout) # 사유 memo_layout = QHBoxLayout() memo_label = QLabel(tr('view.leave.field_reason')) memo_label.setObjectName("field_label") memo_label.setFixedWidth(40) self.memo_input = QLineEdit() self.memo_input.setPlaceholderText(tr('view.leave.placeholder_reason')) memo_layout.addWidget(memo_label) memo_layout.addWidget(self.memo_input) layout.addLayout(memo_layout) # 안내 info_label = QLabel(tr('view.leave.note_auto_deduct')) info_label.setObjectName("note_text") layout.addWidget(info_label) # 버튼 button_layout = QHBoxLayout() save_button = QPushButton(tr('btn.save')) save_button.setObjectName("btn_primary") save_button.clicked.connect(self.save_record) button_layout.addWidget(save_button) cancel_button = QPushButton(tr('btn.cancel')) cancel_button.clicked.connect(self.reject) button_layout.addWidget(cancel_button) layout.addLayout(button_layout) self.setLayout(layout) def on_type_changed(self, index): """연차 유형 변경 시""" leave_type = self.type_combo.currentData() # 시간 연차일 때만 시간 입력 활성화 self.hours_spin.setEnabled(leave_type == "hourly") def save_record(self): """기록 저장""" date = self.date_edit.date().toString("yyyy-MM-dd") leave_type = self.type_combo.currentData() leave_type_name = self.type_combo.currentText() memo = self.memo_input.text().strip() # 사용 일수 계산 if leave_type == "annual": days = 1.0 elif leave_type == "half": days = 0.5 elif leave_type == "quarter": days = 0.25 elif leave_type == "hourly": hours = self.hours_spin.value() days = hours / 8.0 else: days = 1.0 # 잔여 연차 확인 current_balance = self.db.get_leave_balance() if current_balance < days: QMessageBox.warning( self, tr('view.leave.short_title'), tr('view.leave.short_body', balance=current_balance, req=days) ) return # 휴일/주말 검증 — 차감 의미 없으므로 차단 from datetime import datetime as _dt date_dt = _dt.strptime(date, "%Y-%m-%d") if date_dt.weekday() in (5, 6): # 토/일 QMessageBox.warning( self, "주말 등록 불가", "주말에는 연차를 등록할 수 없습니다. (이미 비근무일)" ) return if self.db.is_holiday(date): holiday = self.db.get_holiday(date) name = (holiday or {}).get('name', '공휴일') QMessageBox.warning( self, "공휴일 등록 불가", f"{date}는 이미 공휴일({name})입니다.\n연차를 차감할 필요가 없습니다." ) return # 같은 날 중복 누적 검증 (이미 등록된 + 신규 days <= 1.0) existing_min = self.db.get_leave_minutes_for(date) existing_days = existing_min / max(1, self.db.get_work_minutes()) if existing_days + days > 1.0001: # 부동소수점 여유 QMessageBox.warning( self, "중복 등록 초과", f"{date}에 이미 {existing_days:.2f}일이 등록되어 있어\n" f"추가 {days:.2f}일을 더하면 1일을 초과합니다." ) return # 확인 메시지 hours = days * 8 reply = QMessageBox.question( self, tr('view.leave.confirm_title'), tr('view.leave.confirm_body', date=date, type=leave_type_name, days=days, hours=hours, reason=(memo if memo else '-')), QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: try: # 연차 사용 기록 추가 (파라미터 순서: days, date, leave_type, memo) self.db.use_leave(days, date, leave_type_name, memo) QMessageBox.information( self, tr('view.leave.added_title'), tr('view.leave.added_body', days=days, hours=hours) ) self.accept() except Exception as e: QMessageBox.critical( self, tr('view.leave.error_title'), tr('view.leave.error_body', err=str(e)) ) # 테스트 코드 if __name__ == "__main__": from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) dialog = LeaveView() dialog.exec_()