""" テーマ対応マスタ管理アプリケーション UI統一 + SQLiteテーマ管理 """ import flet as ft import sqlite3 import signal import sys import logging from datetime import datetime from typing import List, Dict, Optional class ThemeManager: """テーマ管理クラス""" def __init__(self): self.themes = {} self.current_theme = None self._load_themes() def _load_themes(self): """テーマ情報をSQLiteから読み込む""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() # テーマテーブル作成 cursor.execute(''' CREATE TABLE IF NOT EXISTS themes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, title TEXT NOT NULL, table_name TEXT NOT NULL, fields TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # デフォルトテーマを挿入 default_themes = [ { 'name': 'customers', 'title': '顧客マスタ', 'table_name': 'customers', 'fields': '[{"name": "name", "label": "顧客名", "width": 200, "required": true}, {"name": "phone", "label": "電話番号", "width": 200}, {"name": "email", "label": "メールアドレス", "width": 250}, {"name": "address", "label": "住所", "width": 300}]' }, { 'name': 'products', 'title': '商品マスタ', 'table_name': 'products', 'fields': '[{"name": "name", "label": "商品名", "width": 200, "required": true}, {"name": "category", "label": "カテゴリ", "width": 150}, {"name": "price", "label": "価格", "width": 100, "keyboard_type": "number", "required": true}, {"name": "stock", "label": "在庫数", "width": 100, "keyboard_type": "number"}]' }, { 'name': 'sales_slips', 'title': '伝票マスタ', 'table_name': 'sales_slips', 'fields': '[{"name": "title", "label": "伝票タイトル", "width": 300, "required": true}, {"name": "customer_name", "label": "顧客名", "width": 200, "required": true}, {"name": "items", "label": "明細", "width": 400, "keyboard_type": "multiline", "required": true}, {"name": "total_amount", "label": "合計金額", "width": 150, "keyboard_type": "number", "required": true}]' } ] # デフォルトテーマがなければ挿入 cursor.execute("SELECT COUNT(*) FROM themes") if cursor.fetchone()[0] == 0: for theme in default_themes: cursor.execute(''' INSERT INTO themes (name, title, table_name, fields) VALUES (?, ?, ?, ?) ''', (theme['name'], theme['title'], theme['table_name'], theme['fields'])) conn.commit() # テーマを読み込み cursor.execute("SELECT * FROM themes ORDER BY id") themes_data = cursor.fetchall() for theme_data in themes_data: theme_id, name, title, table_name, fields_json, created_at = theme_data self.themes[name] = { 'id': theme_id, 'title': title, 'table_name': table_name, 'fields': eval(fields_json) # JSONをPythonオブジェクトに変換 } conn.close() logging.info(f"テーマ読込完了: {len(self.themes)}個") except Exception as e: logging.error(f"テーマ読込エラー: {e}") def get_theme_names(self) -> List[str]: """テーマ名リストを取得""" return list(self.themes.keys()) def get_theme(self, name: str) -> Dict: """指定されたテーマを取得""" return self.themes.get(name, {}) class UniversalMasterEditor: """汎用マスタ編集コンポーネント""" def __init__(self, page: ft.Page): self.page = page self.theme_manager = ThemeManager() # 現在のテーマ self.current_theme_name = 'customers' self.current_theme = self.theme_manager.get_theme(self.current_theme_name) self.current_data = [] self.editing_id = None # UI部品 self.title = ft.Text("", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) self.theme_dropdown = ft.Dropdown( label="マスタ選択", options=[], value="customers", on_change=self.change_theme ) # フォーム self.form_fields = ft.Column([], spacing=10) # ボタン群 self.add_btn = ft.Button("追加", on_click=self.add_new, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) self.save_btn = ft.Button("保存", on_click=self.save_data, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE) self.delete_btn = ft.Button("削除", on_click=self.delete_selected, bgcolor=ft.Colors.RED, color=ft.Colors.WHITE) self.clear_btn = ft.Button("クリア", on_click=self.clear_form, bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE) # データリスト self.data_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300) # 操作説明 self.instructions = ft.Text( "操作方法: マスタを選択してデータを編集・追加・削除", size=12, color=ft.Colors.GREY_600 ) # 初期化 self._update_theme_dropdown() self._build_form() self._load_data() def _update_theme_dropdown(self): """テーマドロップダウンを更新""" theme_names = self.theme_manager.get_theme_names() self.theme_dropdown.options = [ ft.dropdown.Option(theme['title'], name) for name, theme in self.theme_manager.themes.items() ] self.page.update() def change_theme(self, e): """テーマを切り替え""" self.current_theme_name = e.control.value self.current_theme = self.theme_manager.get_theme(self.current_theme_name) self.editing_id = None self.title.value = self.current_theme['title'] self._build_form() self._load_data() self.page.update() def _build_form(self): """入力フォームを構築""" self.form_fields.controls.clear() for field in self.current_theme['fields']: keyboard_type = ft.KeyboardType.TEXT if field.get('keyboard_type') == 'number': keyboard_type = ft.KeyboardType.NUMBER elif field.get('keyboard_type') == 'multiline': keyboard_type = ft.KeyboardType.MULTILINE field_control = ft.TextField( label=field['label'], value='', width=field['width'], keyboard_type=keyboard_type ) self.form_fields.controls.append(field_control) self.page.update() def _load_data(self): """データを読み込む""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() table_name = self.current_theme['table_name'] cursor.execute(f''' SELECT * FROM {table_name} ORDER BY id ''') self.current_data = cursor.fetchall() conn.close() # データリストを更新 self.data_list.controls.clear() for item in self.current_data: item_id = item[0] item_data = dict(zip([col[0] for col in cursor.description], item)) # データ行 row_controls = [] for field in self.current_theme['fields']: field_name = field['name'] value = item_data.get(field_name, '') row_controls.append( ft.Text(f"{field['label']}: {value}", size=12) ) # 操作ボタン row_controls.extend([ ft.Button( "編集", on_click=lambda _, did=item_id: self.edit_item(item_id), bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE, width=60 ), ft.Button( "削除", on_click=lambda _, did=item_id: self.delete_item(item_id), bgcolor=ft.Colors.RED, color=ft.Colors.WHITE, width=60 ) ]) # データカード data_card = ft.Card( content=ft.Container( content=ft.Column(row_controls), padding=10 ), margin=ft.margin.only(bottom=5) ) self.data_list.controls.append(data_card) self.page.update() except Exception as e: logging.error(f"{self.current_theme_name}データ読込エラー: {e}") def save_data(self, e): """データを保存""" try: # フォームデータを収集 form_data = {} for i, field in enumerate(self.current_theme['fields']): field_control = self.form_fields.controls[i] form_data[field['name']] = field_control.value # 必須項目チェック if field.get('required', False) and not field_control.value.strip(): self._show_snackbar(f"{field['label']}は必須項目です", ft.Colors.RED) return # データベースに保存 conn = sqlite3.connect('sales.db') cursor = conn.cursor() table_name = self.current_theme['table_name'] if self.editing_id: # 更新 columns = ', '.join([f"{key} = ?" for key in form_data.keys()]) cursor.execute(f''' UPDATE {table_name} SET {columns} WHERE id = ? ''', tuple(form_data.values()) + (self.editing_id,)) self._show_snackbar("データを更新しました", ft.Colors.GREEN) logging.info(f"{table_name}データ更新完了: ID={self.editing_id}") else: # 新規追加 columns = ', '.join([field['name'] for field in self.current_theme['fields']]) placeholders = ', '.join(['?' for _ in self.current_theme['fields']]) cursor.execute(f''' INSERT INTO {table_name} ({columns}) VALUES ({placeholders}) ''', tuple(form_data.values())) self._show_snackbar("データを追加しました", ft.Colors.GREEN) logging.info(f"{table_name}データ追加完了") conn.commit() conn.close() # フォームをクリア self.clear_form(None) # データ再読み込み self._load_data() except Exception as e: logging.error(f"{self.current_theme_name}データ保存エラー: {e}") self._show_snackbar("保存エラー", ft.Colors.RED) def add_new(self, e): """新規データを追加""" self.editing_id = None self.clear_form(None) self.form_fields.controls[0].focus() logging.info(f"{self.current_theme_name}新規追加モード") def edit_item(self, item_id): """データを編集""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() table_name = self.current_theme['table_name'] cursor.execute(f''' SELECT * FROM {table_name} WHERE id = ? ''', (item_id,)) result = cursor.fetchone() conn.close() if result: self.editing_id = item_id # フォームにデータを設定 for i, field in enumerate(self.current_theme['fields']): field_control = self.form_fields.controls[i] field_control.value = result[i+1] if i+1 < len(result) else '' self.page.update() logging.info(f"{table_name}編集モード: ID={item_id}") except Exception as e: logging.error(f"{self.current_theme_name}編集エラー: {e}") def delete_item(self, item_id): """データを削除""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() table_name = self.current_theme['table_name'] cursor.execute(f''' DELETE FROM {table_name} WHERE id = ? ''', (item_id,)) conn.commit() conn.close() # データ再読み込み self._load_data() self._show_snackbar("データを削除しました", ft.Colors.GREEN) logging.info(f"{table_name}削除完了: ID={item_id}") except Exception as e: logging.error(f"{self.current_theme_name}削除エラー: {e}") self._show_snackbar("削除エラー", ft.Colors.RED) def delete_selected(self, e): """選択中のデータを削除""" if self.editing_id: self.delete_item(self.editing_id) else: self._show_snackbar("削除対象が選択されていません", ft.Colors.ORANGE) def clear_form(self, e): """フォームをクリア""" for field_control in self.form_fields.controls: field_control.value = '' self.editing_id = None self.page.update() logging.info("フォームをクリア") def _show_snackbar(self, message: str, color: ft.Colors): """SnackBarを表示""" try: self.page.snack_bar = ft.SnackBar( content=ft.Text(message), bgcolor=color ) self.page.snack_bar.open = True self.page.update() except: pass def build(self): """UIを構築して返す""" return ft.Column([ ft.Row([ self.title, self.theme_dropdown ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), ft.Divider(), ft.Text("入力フォーム", size=18, weight=ft.FontWeight.BOLD), self.form_fields, ft.Row([self.add_btn, self.save_btn, self.delete_btn, self.clear_btn], spacing=10), ft.Divider(), ft.Text(f"{self.current_theme['title']}一覧", size=18, weight=ft.FontWeight.BOLD), self.instructions, self.data_list ], expand=True, spacing=15) class ThemeMasterApp: """テーマ対応マスタ管理アプリケーション""" def __init__(self, page: ft.Page): self.page = page # ログ設定 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) # データベース初期化 self._init_database() # ウィンドウ設定 page.title = "テーマ対応マスタ管理システム" page.window_width = 1000 page.window_height = 700 page.theme_mode = ft.ThemeMode.LIGHT # ウィンドウクローズイベント page.on_window_close = lambda _: signal_handler(0, None) # テーマ対応マスタエディタ作成 self.theme_editor = UniversalMasterEditor(page) # ページ構築 page.add( self.theme_editor.build() ) logging.info("テーマ対応マスタ管理システム起動完了") print("🚀 テーマ対応マスタ管理システム起動完了") 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, updated_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, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS sales_slips ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, customer_name TEXT NOT NULL, items TEXT NOT NULL, total_amount REAL NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() logging.info("マスタデータベース初期化完了") except Exception as e: logging.error(f"データベース初期化エラー: {e}") def main(page: ft.Page): """メイン関数""" try: app = ThemeMasterApp(page) except Exception as e: logging.error(f"アプリケーション起動エラー: {e}") if __name__ == "__main__": ft.run(main)