""" 汎用マスタ編集コンポーネント 1つのコンポーネントで複数のマスタを管理 """ import flet as ft import sqlite3 import logging from datetime import datetime from typing import List, Dict, Optional, Callable class UniversalMasterEditor: """汎用マスタ編集コンポーネント""" def __init__(self, page: ft.Page): self.page = page # マスタ定義 self.masters = { '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} ] }, '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': ft.KeyboardType.NUMBER, 'required': True}, {'name': 'stock', 'label': '在庫数', 'width': 100, 'keyboard_type': ft.KeyboardType.NUMBER} ] }, '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': ft.KeyboardType.MULTILINE, 'required': True}, {'name': 'total_amount', 'label': '合計金額', 'width': 150, 'keyboard_type': ft.KeyboardType.NUMBER, 'required': True} ] } } # 現在のマスタ self.current_master = 'customers' self.current_data = [] self.editing_id = None # UI部品 self.title = ft.Text("", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) self.master_dropdown = ft.Dropdown( label="マスタ選択", options=[ ft.dropdown.Option("顧客マスタ", "customers"), ft.dropdown.Option("商品マスタ", "products"), ft.dropdown.Option("伝票マスタ", "sales_slips") ], value="customers", on_change=self.change_master ) # フォーム 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._build_form() self._load_data() def change_master(self, e): """マスタを切り替え""" self.current_master = e.control.value self.editing_id = None self.title.value = self.masters[self.current_master]['title'] self._build_form() self._load_data() self.page.update() def _build_form(self): """入力フォームを構築""" self.form_fields.controls.clear() for field in self.masters[self.current_master]['fields']: field_control = ft.TextField( label=field['label'], value='', width=field['width'], keyboard_type=field.get('keyboard_type', ft.KeyboardType.TEXT) ) 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.masters[self.current_master]['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.masters[self.current_master]['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_master}データ読込エラー: {e}") def save_data(self, e): """データを保存""" try: # フォームデータを収集 form_data = {} for i, field in enumerate(self.masters[self.current_master]['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.masters[self.current_master]['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.masters[self.current_master]['fields']]) placeholders = ', '.join(['?' for _ in self.masters[self.current_master]['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_master}データ保存エラー: {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_master}新規追加モード") def edit_item(self, item_id): """データを編集""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() table_name = self.masters[self.current_master]['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.masters[self.current_master]['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_master}編集エラー: {e}") def delete_item(self, item_id): """データを削除""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() table_name = self.masters[self.current_master]['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_master}削除エラー: {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.master_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.masters[self.current_master]['title']}一覧", size=18, weight=ft.FontWeight.BOLD), self.instructions, self.data_list ], expand=True, spacing=15) # 使用例 def create_universal_master_editor(page: ft.Page) -> UniversalMasterEditor: """汎用マスタ編集画面を作成""" return UniversalMasterEditor(page)