import flet as ft import sqlite3 import signal import sys import logging import random from datetime import datetime from typing import List, Dict, Optional class ErrorHandler: """グローバルエラーハンドラ""" @staticmethod def handle_error(error: Exception, context: str = ""): """エラーを一元処理""" error_msg = f"{context}: {str(error)}" logging.error(error_msg) print(f"❌ {error_msg}") # SnackBarでユーザーに通知 try: # グローバルページ参照用 if hasattr(ErrorHandler, 'current_page'): ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) except: pass @staticmethod def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): """SnackBarを表示""" try: page.snack_bar = ft.SnackBar( content=ft.Text(message), bgcolor=color ) page.snack_bar.open = True page.update() except: pass class DummyDataGenerator: """テスト用ダミーデータ生成""" @staticmethod def generate_customers(count: int = 100) -> List[Dict]: """ダミー顧客データ生成""" first_names = ["田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本", "中村", "小林", "加藤"] last_names = ["太郎", "次郎", "三郎", "花子", "美子", "健一", "恵子", "大輔", "由美", "翔太"] customers = [] for i in range(count): name = f"{random.choice(first_names)} {random.choice(last_names)}" customers.append({ 'id': i + 1, 'name': name, 'phone': f"090-{random.randint(1000, 9999)}-{random.randint(1000, 9999)}", 'email': f"customer{i+1}@example.com", 'address': f"東京都{random.choice(['渋谷区', '新宿区', '港区', '千代田区'])}{random.randint(1, 50)}-{random.randint(1, 10)}" }) return customers @staticmethod def generate_products(count: int = 50) -> List[Dict]: """ダミー商品データ生成""" categories = ["電子機器", "衣料品", "食品", "書籍", "家具"] products = [] for i in range(count): category = random.choice(categories) products.append({ 'id': i + 1, 'name': f"{category}{i+1}", 'category': category, 'price': random.randint(100, 50000), 'stock': random.randint(0, 100) }) return products @staticmethod def generate_sales(count: int = 200) -> List[Dict]: """ダミー売上データ生成""" customers = DummyDataGenerator.generate_customers(20) products = DummyDataGenerator.generate_products(30) sales = [] for i in range(count): customer = random.choice(customers) product = random.choice(products) quantity = random.randint(1, 10) sales.append({ 'id': i + 1, 'customer_id': customer['id'], 'customer_name': customer['name'], 'product_id': product['id'], 'product_name': product['name'], 'quantity': quantity, 'unit_price': product['price'], 'total_price': quantity * product['price'], 'date': datetime.now().strftime("%Y-%m-%d"), 'created_at': datetime.now().isoformat() }) return sales class NavigationHistory: """ナビゲーション履歴管理""" def __init__(self): self.history: List[Dict] = [] self.max_history = 10 def add_to_history(self, page_name: str, page_data: Dict = None): """履歴に追加""" history_item = { 'page': page_name, 'data': page_data, 'timestamp': datetime.now().isoformat() } # 重複を避ける self.history = [item for item in self.history if item['page'] != page_name] self.history.insert(0, history_item) # 履歴数を制限 if len(self.history) > self.max_history: self.history = self.history[:self.max_history] def get_last_page(self) -> Optional[Dict]: """最後のページを取得""" return self.history[0] if self.history else None def get_history(self) -> List[Dict]: """履歴を取得""" return self.history class SafePageManager: """安全なページマネージャー""" def __init__(self, page: ft.Page): self.page = page self.current_page = None self.navigation_history = NavigationHistory() ErrorHandler.current_page = page # グローバル参照用 def safe_navigate(self, page_name: str, page_builder): """安全なページ遷移""" try: # 現在のページ情報を保存 current_data = {} if self.current_page: current_data = self._get_page_data() self.navigation_history.add_to_history(page_name, current_data) # 新しいページを構築 page_instance = page_builder(self) new_page = page_instance.build() # 安全なページ切り替え self._safe_page_transition(new_page, page_name) except Exception as e: ErrorHandler.handle_error(e, f"ページ遷移エラー ({page_name})") def _get_page_data(self) -> Dict: """現在のページデータを取得""" try: if hasattr(self.current_page, 'get_data'): return self.current_page.get_data() return {} except: return {} def _safe_page_transition(self, new_page, page_name: str): """安全なページ切り替え""" try: # 古いコンテンツをクリア self.page.controls.clear() # 新しいコンテンツを追加 self.page.add(new_page) # 現在のページを更新 self.current_page = new_page # ページを更新 self.page.update() logging.info(f"ページ遷移成功: {page_name}") except Exception as e: ErrorHandler.handle_error(e, f"ページ表示エラー ({page_name})") def go_back(self): """前のページに戻る""" try: history = self.navigation_history.get_history() if len(history) > 1: # 前のページに戻る previous_page = history[1] # ページビルダーを呼び出し if previous_page['page'] == 'dashboard': self.safe_navigate('dashboard', DashboardPage) elif previous_page['page'] == 'sales': self.safe_navigate('sales', SalesPage) elif previous_page['page'] == 'customers': self.safe_navigate('customers', CustomerPage) elif previous_page['page'] == 'products': self.safe_navigate('products', ProductPage) # 履歴を更新 self.navigation_history.history.pop(0) # 現在の履歴を削除 except Exception as e: ErrorHandler.handle_error(e, "戻る処理エラー") class DashboardPage: """ダッシュボードページ""" def __init__(self, page_manager): self.page_manager = page_manager def build(self): """ダッシュボードUI構築""" try: # 統計データ取得 stats = self._get_statistics() return ft.Container( content=ft.Column([ ft.Text("ダッシュボード", size=24, weight=ft.FontWeight.BOLD), ft.Divider(), ft.Row([ self._stat_card("総顧客数", stats['customers'], ft.Colors.BLUE), self._stat_card("総商品数", stats['products'], ft.Colors.GREEN), ], spacing=10), ft.Row([ self._stat_card("総売上件数", stats['sales'], ft.Colors.ORANGE), self._stat_card("総売上高", f"¥{stats['total_sales']:,.0f}", ft.Colors.PURPLE), ], spacing=10), ]), padding=20 ) except Exception as e: ErrorHandler.handle_error(e, "ダッシュボード構築エラー") return ft.Text("ダッシュボード読み込みエラー") def _stat_card(self, title: str, value: str, color: ft.Colors): """統計カード作成""" return ft.Card( content=ft.Container( content=ft.Column([ ft.Text(title, size=16, color=color), ft.Text(value, size=20, weight=ft.FontWeight.BOLD), ]), padding=15 ), width=200 ) def _get_statistics(self) -> Dict: """統計データ取得""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() # 各テーブルの件数取得 cursor.execute("SELECT COUNT(*) FROM customers") customers = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*) FROM products") products = cursor.fetchone()[0] cursor.execute("SELECT COUNT(*), COALESCE(SUM(total_price), 0) FROM sales") sales_result = cursor.fetchone() sales_count = sales_result[0] total_sales = sales_result[1] conn.close() return { 'customers': customers, 'products': products, 'sales': sales_count, 'total_sales': total_sales } except Exception as e: ErrorHandler.handle_error(e, "統計データ取得エラー") return {'customers': 0, 'products': 0, 'sales': 0, 'total_sales': 0} def get_data(self) -> Dict: """ページデータ取得""" return {'page': 'dashboard'} class SalesPage: """売上管理ページ""" def __init__(self, page_manager): self.page_manager = page_manager def build(self): """売上管理UI構築""" try: return ft.Container( content=ft.Column([ ft.Text("売上管理", size=24, weight=ft.FontWeight.BOLD), ft.Divider(), ft.Row([ ft.TextField(label="顧客名", width=200), ft.TextField(label="商品名", width=200), ft.TextField(label="金額", width=150), ft.Button("追加", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE), ]), ft.Divider(), ft.Text("売上一覧", size=18), ft.Container( content=ft.Column([], scroll=ft.ScrollMode.AUTO), height=300 ) ]), padding=20 ) except Exception as e: ErrorHandler.handle_error(e, "売上管理ページ構築エラー") return ft.Text("売上管理ページ読み込みエラー") def get_data(self) -> Dict: """ページデータ取得""" return {'page': 'sales'} class CustomerPage: """顧客管理ページ""" def __init__(self, page_manager): self.page_manager = page_manager def build(self): """顧客管理UI構築""" try: return ft.Container( content=ft.Column([ ft.Text("顧客管理", size=24, weight=ft.FontWeight.BOLD), ft.Divider(), ft.Row([ ft.Button("新規追加", bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE), ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE), ]), ft.Divider(), ft.Text("顧客一覧", size=18), ft.Container( content=ft.Column([], scroll=ft.ScrollMode.AUTO), height=300 ) ]), padding=20 ) except Exception as e: ErrorHandler.handle_error(e, "顧客管理ページ構築エラー") return ft.Text("顧客管理ページ読み込みエラー") def get_data(self) -> Dict: """ページデータ取得""" return {'page': 'customers'} class ProductPage: """商品管理ページ""" def __init__(self, page_manager): self.page_manager = page_manager def build(self): """商品管理UI構築""" try: return ft.Container( content=ft.Column([ ft.Text("商品管理", size=24, weight=ft.FontWeight.BOLD), ft.Divider(), ft.Row([ ft.Button("新規追加", bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE), ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE), ]), ft.Divider(), ft.Text("商品一覧", size=18), ft.Container( content=ft.Column([], scroll=ft.ScrollMode.AUTO), height=300 ) ]), padding=20 ) except Exception as e: ErrorHandler.handle_error(e, "商品管理ページ構築エラー") return ft.Text("商品管理ページ読み込みエラー") def get_data(self) -> Dict: """ページデータ取得""" return {'page': 'products'} class SalesAssistantApp: """メインアプリケーション""" def __init__(self, page: ft.Page): self.page = page self.page_manager = SafePageManager(page) # ログ設定 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('app.log'), logging.StreamHandler() ] ) # シグナルハンドラ設定 signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) # データベース初期化 self._init_database() self._generate_dummy_data() # ナビゲーションバー構築 self._build_navigation() # 初期ページ表示 self.page_manager.safe_navigate('dashboard', DashboardPage) def _signal_handler(self, signum, frame): """シグナルハンドラ""" print(f"\nシグナル {signum} を受信しました") self._cleanup_resources() sys.exit(0) def _cleanup_resources(self): """リソースクリーンアップ""" try: logging.info("アプリケーション終了処理開始") print("✅ 正常終了処理完了") logging.info("アプリケーション正常終了") except Exception as e: logging.error(f"クリーンアップエラー: {e}") print(f"❌ クリーンアップエラー: {e}") def _init_database(self): """データベース初期化""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() # 各テーブル作成 cursor.execute(''' CREATE TABLE IF NOT EXISTS customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, email TEXT, address TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, category TEXT, price REAL NOT NULL, stock INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS sales ( id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER, customer_name TEXT NOT NULL, product_id INTEGER, product_name TEXT NOT NULL, quantity INTEGER NOT NULL, unit_price REAL NOT NULL, total_price REAL NOT NULL, date TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customers(id), FOREIGN KEY (product_id) REFERENCES products(id) ) ''') conn.commit() conn.close() logging.info("データベース初期化完了") except Exception as e: ErrorHandler.handle_error(e, "データベース初期化エラー") def _generate_dummy_data(self): """ダミーデータ生成""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() # 既存データチェック cursor.execute("SELECT COUNT(*) FROM customers") if cursor.fetchone()[0] == 0: print("📊 ダミーデータを生成中...") # 顧客データ customers = DummyDataGenerator.generate_customers(50) for customer in customers: cursor.execute(''' INSERT INTO customers (name, phone, email, address) VALUES (?, ?, ?, ?) ''', (customer['name'], customer['phone'], customer['email'], customer['address'])) # 商品データ products = DummyDataGenerator.generate_products(30) for product in products: cursor.execute(''' INSERT INTO products (name, category, price, stock) VALUES (?, ?, ?, ?) ''', (product['name'], product['category'], product['price'], product['stock'])) # 売上データ sales = DummyDataGenerator.generate_sales(100) for sale in sales: cursor.execute(''' INSERT INTO sales (customer_id, customer_name, product_id, product_name, quantity, unit_price, total_price, date) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', (sale['customer_id'], sale['customer_name'], sale['product_id'], sale['product_name'], sale['quantity'], sale['unit_price'], sale['total_price'], sale['date'])) conn.commit() print("✅ ダミーデータ生成完了") conn.close() except Exception as e: ErrorHandler.handle_error(e, "ダミーデータ生成エラー") def _build_navigation(self): """ナビゲーションバー構築""" try: # ナビゲーションボタン nav_buttons = [ ft.ElevatedButton( "ダッシュボード", on_click=lambda _: self.page_manager.safe_navigate('dashboard', DashboardPage), bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE ), ft.ElevatedButton( "売上管理", on_click=lambda _: self.page_manager.safe_navigate('sales', SalesPage), bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE ), ft.ElevatedButton( "顧客管理", on_click=lambda _: self.page_manager.safe_navigate('customers', CustomerPage), bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE ), ft.ElevatedButton( "商品管理", on_click=lambda _: self.page_manager.safe_navigate('products', ProductPage), bgcolor=ft.Colors.PURPLE, color=ft.Colors.WHITE ), ft.ElevatedButton( "戻る", on_click=lambda _: self.page_manager.go_back(), bgcolor=ft.Colors.RED, color=ft.Colors.WHITE ) ] # ナビゲーションバー self.page.navigation_bar = ft.NavigationBar( destinations=[ ft.NavigationBarDestination( icon=ft.Icons.DASHBOARD, label="ダッシュボード" ), ft.NavigationBarDestination( icon=ft.Icons.SHOPPING_CART, label="売上" ), ft.NavigationBarDestination( icon=ft.Icons.PEOPLE, label="顧客" ), ft.NavigationBarDestination( icon=ft.Icons.INVENTORY, label="商品" ) ], on_change=self._on_nav_change ) # 代替ナビゲーション(NavigationBarが動かない場合) self.page.add( ft.Container( content=ft.Row([ ft.Button( "ダッシュボード", on_click=lambda _: self.page_manager.safe_navigate('dashboard', DashboardPage), bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE ), ft.Button( "売上管理", on_click=lambda _: self.page_manager.safe_navigate('sales', SalesPage), bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE ), ft.Button( "顧客管理", on_click=lambda _: self.page_manager.safe_navigate('customers', CustomerPage), bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE ), ft.Button( "商品管理", on_click=lambda _: self.page_manager.safe_navigate('products', ProductPage), bgcolor=ft.Colors.PURPLE, color=ft.Colors.WHITE ), ft.Button( "戻る", on_click=lambda _: self.page_manager.go_back(), bgcolor=ft.Colors.RED, color=ft.Colors.WHITE ) ], spacing=5), padding=10, bgcolor=ft.Colors.GREY_100 ) ) except Exception as e: ErrorHandler.handle_error(e, "ナビゲーション構築エラー") def _on_nav_change(self, e): """ナビゲーション変更イベント""" try: index = e.control.selected_index pages = [DashboardPage, SalesPage, CustomerPage, ProductPage] page_names = ['dashboard', 'sales', 'customers', 'products'] if 0 <= index < len(pages): self.page_manager.safe_navigate(page_names[index], pages[index]) except Exception as ex: ErrorHandler.handle_error(ex, "ナビゲーション変更エラー") def main(page: ft.Page): """メイン関数""" try: # ウィンドウ設定 page.title = "販売アシスト1号" page.window_width = 800 page.window_height = 600 page.theme_mode = ft.ThemeMode.LIGHT # ウィンドウクローズイベント page.on_window_close = lambda _: SalesAssistantApp(page)._cleanup_resources() # アプリケーション起動 app = SalesAssistantApp(page) logging.info("アプリケーション起動完了") print("🚀 頑健な販売アシスト1号起動完了") except Exception as e: ErrorHandler.handle_error(e, "アプリケーション起動エラー") if __name__ == "__main__": ft.run(main)