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}") 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 def main(page: ft.Page): """メイン関数""" try: # ログ設定 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('app.log'), logging.StreamHandler() ] ) # シグナルハンドラ設定 def signal_handler(signum, frame): print(f"\nシグナル {signum} を受信しました") print("✅ 正常終了処理完了") logging.info("アプリケーション正常終了") sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) # データベース初期化 def init_db(): 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_name TEXT NOT NULL, 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 ) ''') conn.commit() conn.close() logging.info("データベース初期化完了") except Exception as e: logging.error(f"データベース初期化エラー: {e}") print(f"❌ データベース初期化エラー: {e}") # ダミーデータ生成 def generate_dummy_data(): 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_name, product_name, quantity, unit_price, total_price, date) VALUES (?, ?, ?, ?, ?, ?) ''', (sale['customer_name'], sale['product_name'], sale['quantity'], sale['unit_price'], sale['total_price'], sale['date'])) conn.commit() print("✅ ダミーデータ生成完了") conn.close() except Exception as e: logging.error(f"ダミーデータ生成エラー: {e}") print(f"❌ ダミーデータ生成エラー: {e}") # ウィンドウ設定 page.title = "販売アシスト1号 (シンプル版)" page.window_width = 800 page.window_height = 600 page.theme_mode = ft.ThemeMode.LIGHT # ウィンドウクローズイベント page.on_window_close = lambda _: signal_handler(0, None) # データベース初期化とダミーデータ生成 init_db() generate_dummy_data() # 現在のページインデックス current_page_index = [0] # UI要素 title = ft.Text("販売アシスト1号", size=24, weight=ft.FontWeight.BOLD) # フォーム要素 customer_tf = ft.TextField(label="顧客名", width=200, autofocus=True) product_tf = ft.TextField(label="商品名", width=200) amount_tf = ft.TextField(label="金額", width=150, keyboard_type=ft.KeyboardType.NUMBER) # ボタン add_btn = ft.Button("追加", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) # 売上一覧 sales_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300) # 操作説明 instructions = ft.Text( "操作方法: TABでフォーカス移動、SPACE/ENTERで追加、1-4でページ遷移", size=12, color=ft.Colors.GREY_600 ) def load_sales(): """売上データ読み込み""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' SELECT customer_name, product_name, total_price, date FROM sales ORDER BY created_at DESC LIMIT 20 ''') sales = cursor.fetchall() conn.close() # リストを更新 sales_list.controls.clear() for sale in sales: customer, product, amount, date = sale sales_list.controls.append( ft.Text(f"{date}: {customer} - {product}: ¥{amount:,.0f}") ) page.update() except Exception as e: logging.error(f"売上データ読み込みエラー: {e}") def add_sale(e): """売上データ追加""" if customer_tf.value and product_tf.value and amount_tf.value: try: # 保存前に値を取得 customer_val = customer_tf.value product_val = product_tf.value amount_val = float(amount_tf.value) # データベースに保存 conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) VALUES (?, ?, ?, ?, ?, ?) ''', (customer_val, product_val, 1, amount_val, amount_val, datetime.now().strftime("%Y-%m-%d"))) conn.commit() conn.close() # フィールドをクリア customer_tf.value = "" product_tf.value = "" amount_tf.value = "" # リスト更新 load_sales() # 成功メッセージ page.snack_bar = ft.SnackBar( content=ft.Text("保存しました"), bgcolor=ft.Colors.GREEN ) page.snack_bar.open = True page.update() logging.info(f"売上データ追加: {customer_val} {product_val} {amount_val}") except Exception as ex: logging.error(f"売上データ保存エラー: {ex}") page.snack_bar = ft.SnackBar( content=ft.Text("エラーが発生しました"), bgcolor=ft.Colors.RED ) page.snack_bar.open = True page.update() # キーボードイベントハンドラ def on_keyboard(e: ft.KeyboardEvent): # SPACEまたはENTERで追加 if e.key == " " or e.key == "Enter": add_sale(None) # 数字キーでページ遷移(今回はシンプルに) elif e.key == "1": print("ダッシュボードに遷移します") elif e.key == "2": print("売上管理に遷移します") elif e.key == "3": print("顧客管理に遷移します") elif e.key == "4": print("商品管理に遷移します") # ESCで終了 elif e.key == "Escape": signal_handler(0, None) # キーボードイベント設定 page.on_keyboard_event = on_keyboard # 初期データ読み込み load_sales() # ページ構築 page.add( title, ft.Divider(), ft.Text("売上登録", size=18, weight=ft.FontWeight.BOLD), ft.Row([customer_tf, product_tf, amount_tf, add_btn]), ft.Divider(), ft.Text("売上一覧", size=18), instructions, ft.Divider(), sales_list ) logging.info("アプリケーション起動完了") print("🚀 シンプル版販売アシスト1号起動完了") except Exception as e: logging.error(f"アプリケーション起動エラー: {e}") print(f"❌ アプリケーション起動エラー: {e}") if __name__ == "__main__": ft.run(main)