h-1.flet.3/components/universal_master_editor.py

348 lines
13 KiB
Python

"""
汎用マスタ編集コンポーネント
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)