"""Paso 5: equipos sugeridos por el motor de reglas heurísticas.""" from __future__ import annotations from PySide6.QtCore import Property, Qt, Signal from PySide6.QtWidgets import ( QAbstractItemView, QHeaderView, QLabel, QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, QWizardPage, ) from vmssailor.core.enums import SystemId, VesselSubtype, VesselType from vmssailor.studio.designer.rule_engine import ( EquipmentProposal, RuleContext, RuleEngine, ) from vmssailor.studio.theme import C_CYAN, C_FOG, mono_font, ui_font class Step05Equipment(QWizardPage): """Muestra propuestas del rule engine. Integrador acepta/rechaza.""" proposalsChanged = Signal() def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self.setTitle("Paso 5 · Equipos sugeridos") self.setSubTitle( "El motor de reglas propuso estos equipos basado en el tipo de buque y " "los sistemas habilitados. Desmarca los que no apliquen." ) layout = QVBoxLayout(self) layout.setSpacing(12) intro = QLabel( "Doble-click en una celda para editar. La columna ✓ permite excluir propuestas." ) intro.setStyleSheet(f"color: {C_FOG};") intro.setFont(ui_font(10)) layout.addWidget(intro) self._table = QTableWidget(0, 7) self._table.setHorizontalHeaderLabels( ["✓", "Sistema", "Modelo", "Tag prefix", "x_pp", "y_cl", "Rationale"] ) self._table.horizontalHeader().setSectionResizeMode( 6, QHeaderView.ResizeMode.Stretch ) self._table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeMode.ResizeToContents ) self._table.setAlternatingRowColors(False) self._table.verticalHeader().setVisible(False) self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setEditTriggers( QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.EditKeyPressed ) self._table.itemChanged.connect(self._on_item_changed) layout.addWidget(self._table, 1) # Counter label self._counter = QLabel("0 propuestas") self._counter.setFont(mono_font(10)) self._counter.setStyleSheet(f"color: {C_CYAN};") layout.addWidget(self._counter) btn_row = QPushButton("Regenerar desde reglas") btn_row.clicked.connect(self._regenerate) layout.addWidget(btn_row) self._proposals: list[EquipmentProposal] = [] from vmssailor.studio.wizard.wizard import F_PROPOSALS self.registerField(F_PROPOSALS, self, "proposals", "proposalsChanged") # ----- Property API ------------------------------------------------ def get_proposals(self) -> list[EquipmentProposal]: return [p for p in self._proposals if p.accepted] def set_proposals(self, value: list[EquipmentProposal]) -> None: self._proposals = value self._refresh_table() self.proposalsChanged.emit() proposals = Property( list, fget=get_proposals, fset=set_proposals, notify=proposalsChanged ) # ----- Lifecycle --------------------------------------------------- def initializePage(self) -> None: if not self._proposals: self._regenerate() def _regenerate(self) -> None: from vmssailor.studio.wizard.wizard import ( F_BEAM, F_DRAFT, F_LOA, F_SYSTEMS, F_VESSEL_SUBTYPE, F_VESSEL_TYPE, ) try: v_type = VesselType(self.field(F_VESSEL_TYPE) or "yacht_motor") except ValueError: v_type = VesselType.YACHT_MOTOR try: v_sub = VesselSubtype(self.field(F_VESSEL_SUBTYPE) or "planing") except ValueError: v_sub = VesselSubtype.PLANING loa = float(self.field(F_LOA) or 24.0) beam = float(self.field(F_BEAM) or 5.5) draft = float(self.field(F_DRAFT) or 1.8) systems_raw = self.field(F_SYSTEMS) or [] systems: list[SystemId] = [] for s in systems_raw: try: systems.append(SystemId(s)) except ValueError: continue ctx = RuleContext( vessel_type=v_type, vessel_subtype=v_sub, length_overall_m=loa, beam_max_m=beam, draft_m=draft, systems_enabled=systems, ) engine = RuleEngine() self._proposals = engine.propose(ctx) self._refresh_table() self.proposalsChanged.emit() # ----- Table rendering -------------------------------------------- def _refresh_table(self) -> None: self._table.blockSignals(True) self._table.setRowCount(len(self._proposals)) for row, p in enumerate(self._proposals): check = QTableWidgetItem() check.setFlags(check.flags() | Qt.ItemFlag.ItemIsUserCheckable) check.setCheckState(Qt.CheckState.Checked if p.accepted else Qt.CheckState.Unchecked) self._table.setItem(row, 0, check) self._table.setItem(row, 1, QTableWidgetItem(p.system_id.value)) self._table.setItem(row, 2, QTableWidgetItem(p.model_ref)) self._table.setItem(row, 3, QTableWidgetItem(p.tag_prefix)) self._table.setItem(row, 4, QTableWidgetItem(f"{p.location_x_pp:.2f}")) self._table.setItem(row, 5, QTableWidgetItem(f"{p.location_y_cl:.2f}")) rationale = QTableWidgetItem(p.rationale) rationale.setForeground(Qt.GlobalColor.gray) self._table.setItem(row, 6, rationale) self._table.resizeColumnsToContents() self._table.blockSignals(False) accepted = sum(1 for p in self._proposals if p.accepted) self._counter.setText( f"{accepted} aceptadas · {len(self._proposals) - accepted} rechazadas · " f"{len(self._proposals)} totales" ) def _on_item_changed(self, item: QTableWidgetItem) -> None: row = item.row() if row >= len(self._proposals): return col = item.column() p = self._proposals[row] try: if col == 0: p.accepted = item.checkState() == Qt.CheckState.Checked elif col == 2: p.model_ref = item.text() elif col == 3: p.tag_prefix = item.text().upper() item.setText(p.tag_prefix) elif col == 4: p.location_x_pp = float(item.text()) elif col == 5: p.location_y_cl = float(item.text()) except ValueError: # Reset celda al valor previo si la conversión falla self._refresh_table() return accepted = sum(1 for pr in self._proposals if pr.accepted) self._counter.setText( f"{accepted} aceptadas · {len(self._proposals) - accepted} rechazadas · " f"{len(self._proposals)} totales" ) self.proposalsChanged.emit()