""" Main window. Deliberately thin: it wires the toolbar, tabs, and dialogs together, and delegates real work to app.services (fetching), app.workers (saving/loading/stats), and app.services.odoo_export (file export). JIRA and ShipStation are pulled independently (separate buttons, separate workers) per how the business actually operates - they are correlated afterwards for the dashboard via ticket_number, not forced into one pipeline. """ from __future__ import annotations from PyQt6.QtGui import QAction from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QToolBar, QStatusBar, QMessageBox, QLabel, QTabWidget, QFileDialog, ) from app.services import SERVICE_REGISTRY from app.services.odoo_export import export_orders_to_csv from app.ui.settings_dialog import SettingsDialog from app.ui.widgets.orders_table import OrdersTableView from app.ui.widgets.dashboard import DashboardWidget from app.ui.widgets.order_detail_dialog import OrderDetailDialog from app.workers import FetchOrdersWorker, load_all_orders, get_dashboard_stats class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Order Manager") self.resize(1150, 700) self._workers: dict[str, FetchOrdersWorker] = {} self._import_actions: dict[str, QAction] = {} self._build_ui() self._refresh_everything() # -- UI construction ------------------------------------------------- def _build_ui(self) -> None: central = QWidget() layout = QVBoxLayout(central) self.tabs = QTabWidget() self.dashboard = DashboardWidget() self.orders_table = OrdersTableView() self.orders_table.order_double_clicked.connect(self._on_order_double_clicked) self.tabs.addTab(self.dashboard, "Dashboard") self.tabs.addTab(self.orders_table, "All Orders") layout.addWidget(self.tabs) self.setCentralWidget(central) toolbar = QToolBar("Main") toolbar.setMovable(False) self.addToolBar(toolbar) jira_action = QAction("Import from JIRA", self) jira_action.triggered.connect(lambda: self._on_import_clicked("jira")) toolbar.addAction(jira_action) self._import_actions["jira"] = jira_action shipstation_action = QAction("Import from ShipStation", self) shipstation_action.triggered.connect(lambda: self._on_import_clicked("shipstation")) toolbar.addAction(shipstation_action) self._import_actions["shipstation"] = shipstation_action toolbar.addSeparator() export_action = QAction("Export Visible Orders to Odoo CSV", self) export_action.triggered.connect(self._on_export_clicked) toolbar.addAction(export_action) toolbar.addSeparator() settings_action = QAction("Settings", self) settings_action.triggered.connect(self._on_settings_clicked) toolbar.addAction(settings_action) refresh_action = QAction("Refresh from Local DB", self) refresh_action.triggered.connect(self._refresh_everything) toolbar.addAction(refresh_action) self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_label = QLabel("Ready.") self.status_bar.addWidget(self.status_label) # -- actions ----------------------------------------------------------- def _on_settings_clicked(self) -> None: dialog = SettingsDialog(self) dialog.exec() def _on_order_double_clicked(self, order) -> None: dialog = OrderDetailDialog(order, self) dialog.exec() def _on_import_clicked(self, service_name: str) -> None: service_cls = SERVICE_REGISTRY[service_name] service = service_cls() if not service.is_configured(): QMessageBox.warning( self, f"{service_name.title()} not configured", f"Fill in the {service_name.title()} settings first, then try again.", ) return action = self._import_actions[service_name] action.setEnabled(False) self.status_label.setText(f"Importing orders from {service_name.title()}...") worker = FetchOrdersWorker(service) worker.finished_ok.connect( lambda new_count, updated_count: self._on_import_finished( service_name, new_count, updated_count ) ) worker.failed.connect(lambda message: self._on_import_failed(service_name, message)) self._workers[service_name] = worker # keep a reference so it isn't garbage collected worker.start() def _on_import_finished(self, service_name: str, new_count: int, updated_count: int) -> None: self._import_actions[service_name].setEnabled(True) self.status_label.setText( f"{service_name.title()} import complete: {new_count} new, {updated_count} updated." ) self._refresh_everything() def _on_import_failed(self, service_name: str, message: str) -> None: self._import_actions[service_name].setEnabled(True) self.status_label.setText(f"{service_name.title()} import failed.") QMessageBox.critical(self, f"{service_name.title()} import failed", message) def _on_export_clicked(self) -> None: orders = self.orders_table.visible_orders() if not orders: QMessageBox.information( self, "Nothing to export", "No orders match the current filters." ) return filepath, _ = QFileDialog.getSaveFileName( self, "Export to Odoo CSV", "odoo_orders_export.csv", "CSV Files (*.csv)" ) if not filepath: return try: count = export_orders_to_csv(orders, filepath) except OSError as exc: QMessageBox.critical(self, "Export failed", str(exc)) return self.status_label.setText(f"Exported {count} order(s) to {filepath}") def _refresh_everything(self) -> None: orders = load_all_orders() self.orders_table.set_orders(orders) self.dashboard.update_stats(get_dashboard_stats()) if not hasattr(self, "_suppress_ready_status"): self.status_label.setText(f"{len(orders)} order(s) in local database.") # --------------------------------------------------------------------------- # Future growth notes: # # - Odoo push API (once ready): add app/services/odoo_service.py with a # push_orders(orders) method, wire a new toolbar action similarly to # _on_import_clicked, and it can eventually replace/augment odoo_export.py. # - Order detail view: connect a table double-click to a dialog showing # selected_order().raw_data (full JIRA/ShipStation payload) for debugging # ticket-number/SKU extraction as real data comes in. # - Pipeline actions ("mark fulfilled", "create ShipStation label"): add # toolbar actions gated on table selection. # - If the window grows too much, split each tab's toolbar into its own # QToolBar shown only while that tab is active. # ---------------------------------------------------------------------------