771 lines
27 KiB
Python
771 lines
27 KiB
Python
"""
|
|
階層構造商品マスタコンポーネント
|
|
入れ子構造とPDF巨大カッコ表示に対応
|
|
"""
|
|
|
|
import flet as ft
|
|
import sqlite3
|
|
import json
|
|
from typing import List, Dict, Optional
|
|
from datetime import datetime
|
|
|
|
class ProductNode:
|
|
"""商品ノードクラス"""
|
|
|
|
def __init__(self, id: int = None, name: str = "", parent_id: int = None,
|
|
level: int = 0, is_category: bool = True, price: float = 0.0,
|
|
stock: int = 0, description: str = ""):
|
|
self.id = id
|
|
self.name = name
|
|
self.parent_id = parent_id
|
|
self.level = level
|
|
self.is_category = is_category
|
|
self.price = price
|
|
self.stock = stock
|
|
self.description = description
|
|
self.children: List['ProductNode'] = []
|
|
self.expanded = True
|
|
|
|
def add_child(self, child: 'ProductNode'):
|
|
"""子ノードを追加"""
|
|
child.parent_id = self.id
|
|
child.level = self.level + 1
|
|
self.children.append(child)
|
|
|
|
def remove_child(self, child_id: int):
|
|
"""子ノードを削除"""
|
|
self.children = [child for child in self.children if child.id != child_id]
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""辞書に変換"""
|
|
return {
|
|
'id': self.id,
|
|
'name': self.name,
|
|
'parent_id': self.parent_id,
|
|
'level': self.level,
|
|
'is_category': self.is_category,
|
|
'price': self.price,
|
|
'stock': self.stock,
|
|
'description': self.description,
|
|
'expanded': self.expanded,
|
|
'children': [child.to_dict() for child in self.children]
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> 'ProductNode':
|
|
"""辞書から作成"""
|
|
node = cls(
|
|
id=data.get('id'),
|
|
name=data.get('name', ''),
|
|
parent_id=data.get('parent_id'),
|
|
level=data.get('level', 0),
|
|
is_category=data.get('is_category', True),
|
|
price=data.get('price', 0.0),
|
|
stock=data.get('stock', 0),
|
|
description=data.get('description', '')
|
|
)
|
|
node.expanded = data.get('expanded', True)
|
|
|
|
for child_data in data.get('children', []):
|
|
child = cls.from_dict(child_data)
|
|
node.add_child(child)
|
|
|
|
return node
|
|
|
|
class HierarchicalProductMaster:
|
|
"""階層構造商品マスタエディタ"""
|
|
|
|
def __init__(self, page: ft.Page):
|
|
self.page = page
|
|
self.root_nodes: List[ProductNode] = []
|
|
self.selected_node: Optional[ProductNode] = None
|
|
self.next_id = 1
|
|
|
|
# UIコンポーネント
|
|
self.tree_view = ft.Column([], expand=True, spacing=2)
|
|
self.preview_area = ft.Column([], expand=True, spacing=5)
|
|
|
|
# 入力フィールド
|
|
self.name_field = ft.TextField(
|
|
label="商品名/カテゴリ名",
|
|
width=300,
|
|
autofocus=True
|
|
)
|
|
self.price_field = ft.TextField(
|
|
label="価格",
|
|
width=150,
|
|
value="0"
|
|
)
|
|
self.stock_field = ft.TextField(
|
|
label="在庫数",
|
|
width=150,
|
|
value="0"
|
|
)
|
|
self.description_field = ft.TextField(
|
|
label="説明",
|
|
width=400,
|
|
multiline=True,
|
|
height=80
|
|
)
|
|
self.is_category_checkbox = ft.Checkbox(
|
|
label="カテゴリとして扱う",
|
|
value=True
|
|
)
|
|
|
|
# ボタン
|
|
self.add_btn = ft.Button(
|
|
"追加",
|
|
on_click=self.add_node,
|
|
bgcolor=ft.Colors.GREEN,
|
|
color=ft.Colors.WHITE
|
|
)
|
|
self.update_btn = ft.Button(
|
|
"更新",
|
|
on_click=self.update_node,
|
|
bgcolor=ft.Colors.BLUE,
|
|
color=ft.Colors.WHITE
|
|
)
|
|
self.delete_btn = ft.Button(
|
|
"削除",
|
|
on_click=self.delete_node,
|
|
bgcolor=ft.Colors.RED,
|
|
color=ft.Colors.WHITE
|
|
)
|
|
self.preview_btn = ft.Button(
|
|
"PDFプレビュー",
|
|
on_click=self.show_pdf_preview,
|
|
bgcolor=ft.Colors.ORANGE,
|
|
color=ft.Colors.WHITE
|
|
)
|
|
|
|
# データベース初期化
|
|
self._init_database()
|
|
|
|
# サンプルデータ作成
|
|
self._create_sample_data()
|
|
|
|
def _init_database(self):
|
|
"""データベース初期化"""
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
# 階層商品マスタテーブル作成
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS hierarchical_products (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
parent_id INTEGER,
|
|
level INTEGER DEFAULT 0,
|
|
is_category BOOLEAN DEFAULT 1,
|
|
price REAL DEFAULT 0.0,
|
|
stock INTEGER DEFAULT 0,
|
|
description TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (parent_id) REFERENCES hierarchical_products (id)
|
|
)
|
|
''')
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
print(f"データベース初期化エラー: {e}")
|
|
|
|
def _create_sample_data(self):
|
|
"""サンプルデータ作成"""
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
# 既存データチェック
|
|
cursor.execute("SELECT COUNT(*) FROM hierarchical_products")
|
|
if cursor.fetchone()[0] > 0:
|
|
conn.close()
|
|
return
|
|
|
|
print("サンプルデータを作成中...")
|
|
|
|
# 食料品カテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("食料品", None, 0, True))
|
|
|
|
food_id = cursor.lastrowid
|
|
|
|
# 生鮮食品サブカテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("生鮮食品", food_id, 1, True))
|
|
|
|
fresh_id = cursor.lastrowid
|
|
|
|
# 野菜カテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("野菜", fresh_id, 2, True))
|
|
|
|
veg_id = cursor.lastrowid
|
|
|
|
# 具体的な野菜商品
|
|
vegetables = [
|
|
("キャベツ", veg_id, 3, False, 198.0, 50, "新鮮なキャベツ"),
|
|
("人参", veg_id, 3, False, 128.0, 80, "甘い人参"),
|
|
("じゃがいも", veg_id, 3, False, 98.0, 100, "ホクホクのじゃがいも"),
|
|
("玉ねぎ", veg_id, 3, False, 88.0, 120, "辛みの少ない玉ねぎ")
|
|
]
|
|
|
|
for veg in vegetables:
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products
|
|
(name, parent_id, level, is_category, price, stock, description)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', veg)
|
|
|
|
# 果物カテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("果物", fresh_id, 2, True))
|
|
|
|
fruit_id = cursor.lastrowid
|
|
|
|
# 具体的な果物商品
|
|
fruits = [
|
|
("りんご", fruit_id, 3, False, 158.0, 60, "シャリシャリのりんご"),
|
|
("バナナ", fruit_id, 3, False, 198.0, 40, "甘いバナナ"),
|
|
("みかん", fruit_id, 3, False, 298.0, 30, "みかん1箱")
|
|
]
|
|
|
|
for fruit in fruits:
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products
|
|
(name, parent_id, level, is_category, price, stock, description)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', fruit)
|
|
|
|
# 加工食品サブカテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("加工食品", food_id, 1, True))
|
|
|
|
processed_id = cursor.lastrowid
|
|
|
|
# 缶詰カテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("缶詰", processed_id, 2, True))
|
|
|
|
can_id = cursor.lastrowid
|
|
|
|
# 具体的な缶詰商品
|
|
cans = [
|
|
("ツナ缶", can_id, 3, False, 128.0, 100, "水煮ツナ缶"),
|
|
("コーン缶", can_id, 3, False, 98.0, 80, "スイートコーン缶"),
|
|
("ミックスビーンズ", can_id, 3, False, 158.0, 60, "ミックスビーンズ缶")
|
|
]
|
|
|
|
for can in cans:
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products
|
|
(name, parent_id, level, is_category, price, stock, description)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', can)
|
|
|
|
# 日用品カテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("日用品", None, 0, True))
|
|
|
|
daily_id = cursor.lastrowid
|
|
|
|
# 衛生用品サブカテゴリ
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products (name, parent_id, level, is_category)
|
|
VALUES (?, ?, ?, ?)
|
|
''', ("衛生用品", daily_id, 1, True))
|
|
|
|
hygiene_id = cursor.lastrowid
|
|
|
|
# 具体的な衛生用品商品
|
|
hygiene_items = [
|
|
("ティッシュペーパー", hygiene_id, 2, False, 198.0, 200, "柔らかいティッシュ"),
|
|
("トイレットペーパー", hygiene_id, 2, False, 298.0, 100, "ダブルロール12個入"),
|
|
("ハンドソープ", hygiene_id, 2, False, 398.0, 50, "除菌ハンドソープ")
|
|
]
|
|
|
|
for item in hygiene_items:
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products
|
|
(name, parent_id, level, is_category, price, stock, description)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', item)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
print("サンプルデータ作成完了")
|
|
|
|
except Exception as e:
|
|
print(f"サンプルデータ作成エラー: {e}")
|
|
|
|
def load_data(self):
|
|
"""データベースからデータを読み込み"""
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT id, name, parent_id, level, is_category, price, stock, description
|
|
FROM hierarchical_products
|
|
ORDER BY level, parent_id, name
|
|
''')
|
|
|
|
rows = cursor.fetchall()
|
|
conn.close()
|
|
|
|
# ノードマップ作成
|
|
node_map = {}
|
|
self.root_nodes = []
|
|
|
|
for row in rows:
|
|
node = ProductNode(
|
|
id=row[0],
|
|
name=row[1],
|
|
parent_id=row[2],
|
|
level=row[3],
|
|
is_category=bool(row[4]),
|
|
price=row[5],
|
|
stock=row[6],
|
|
description=row[7]
|
|
)
|
|
node_map[node.id] = node
|
|
|
|
# 階層構造構築
|
|
for node in node_map.values():
|
|
if node.parent_id is None:
|
|
self.root_nodes.append(node)
|
|
else:
|
|
parent = node_map.get(node.parent_id)
|
|
if parent:
|
|
parent.add_child(node)
|
|
|
|
# 次のIDを設定
|
|
if rows:
|
|
self.next_id = max(row[0] for row in rows) + 1
|
|
|
|
self.update_tree_view()
|
|
|
|
except Exception as e:
|
|
print(f"データ読み込みエラー: {e}")
|
|
|
|
def save_data(self):
|
|
"""データベースに保存"""
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
# 既存データ削除
|
|
cursor.execute("DELETE FROM hierarchical_products")
|
|
|
|
# 再帰的に保存
|
|
def save_node(node: ProductNode):
|
|
cursor.execute('''
|
|
INSERT INTO hierarchical_products
|
|
(id, name, parent_id, level, is_category, price, stock, description)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
node.id, node.name, node.parent_id, node.level,
|
|
node.is_category, node.price, node.stock, node.description
|
|
))
|
|
|
|
for child in node.children:
|
|
save_node(child)
|
|
|
|
for root in self.root_nodes:
|
|
save_node(root)
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
print(f"データ保存エラー: {e}")
|
|
|
|
def update_tree_view(self):
|
|
"""ツリービュー更新"""
|
|
self.tree_view.controls.clear()
|
|
|
|
for root in self.root_nodes:
|
|
self.tree_view.controls.append(self._create_tree_node(root))
|
|
|
|
self.tree_view.update()
|
|
|
|
def _create_tree_node(self, node: ProductNode) -> ft.Container:
|
|
"""ツリーノード作成"""
|
|
indent = " " * node.level
|
|
|
|
# アイコン
|
|
if node.is_category:
|
|
if node.expanded and node.children:
|
|
icon = ft.Icons.FOLDER_OPEN
|
|
elif node.children:
|
|
icon = ft.Icons.FOLDER
|
|
else:
|
|
icon = ft.Icons.FOLDER_OUTLINE
|
|
else:
|
|
icon = ft.Icons.SHOPPING_CART
|
|
|
|
# ノードコンテナ
|
|
node_container = ft.Container(
|
|
content=ft.Row([
|
|
# 展開/折りたたみボタン
|
|
ft.IconButton(
|
|
icon=ft.Icons.EXPAND_MORE if node.expanded else ft.Icons.CHEVRON_RIGHT,
|
|
on_click=lambda _, n=node: self.toggle_node(n),
|
|
visible=len(node.children) > 0,
|
|
width=30,
|
|
height=30
|
|
),
|
|
# アイコン
|
|
ft.Icon(
|
|
icon,
|
|
color=ft.Colors.ORANGE if node.is_category else ft.Colors.BLUE,
|
|
size=20
|
|
),
|
|
# 名前
|
|
ft.Text(
|
|
f"{indent}{node.name}",
|
|
size=14,
|
|
weight=ft.FontWeight.BOLD if node.is_category else ft.FontWeight.NORMAL,
|
|
expand=True
|
|
),
|
|
# 価格(商品の場合)
|
|
ft.Text(
|
|
f"¥{node.price:.0f}" if not node.is_category else "",
|
|
size=12,
|
|
color=ft.Colors.GREY_600
|
|
),
|
|
# 在庫(商品の場合)
|
|
ft.Text(
|
|
f"在庫:{node.stock}" if not node.is_category else "",
|
|
size=12,
|
|
color=ft.Colors.GREY_600
|
|
),
|
|
# 選択ボタン
|
|
ft.IconButton(
|
|
icon=ft.Icons.EDIT,
|
|
on_click=lambda _, n=node: self.select_node(n),
|
|
icon_size=16,
|
|
tooltip="編集"
|
|
)
|
|
], spacing=5),
|
|
padding=ft.Padding.symmetric(horizontal=10, vertical=5),
|
|
bgcolor=ft.Colors.BLUE_50 if self.selected_node == node else ft.Colors.TRANSPARENT,
|
|
border_radius=5,
|
|
on_click=lambda _, n=node: self.select_node(n)
|
|
)
|
|
|
|
# 子ノード
|
|
children_container = ft.Column([], spacing=2)
|
|
if node.expanded:
|
|
for child in node.children:
|
|
children_container.controls.append(self._create_tree_node(child))
|
|
|
|
return ft.Column([
|
|
node_container,
|
|
children_container
|
|
], spacing=0)
|
|
|
|
def toggle_node(self, node: ProductNode):
|
|
"""ノード展開/折りたたみ"""
|
|
node.expanded = not node.expanded
|
|
self.update_tree_view()
|
|
|
|
def select_node(self, node: ProductNode):
|
|
"""ノード選択"""
|
|
self.selected_node = node
|
|
|
|
# 入力フィールドに値を設定
|
|
self.name_field.value = node.name
|
|
self.price_field.value = str(node.price)
|
|
self.stock_field.value = str(node.stock)
|
|
self.description_field.value = node.description
|
|
self.is_category_checkbox.value = node.is_category
|
|
|
|
# 価格・在庫フィールドの有効/無効
|
|
self.price_field.disabled = node.is_category
|
|
self.stock_field.disabled = node.is_category
|
|
|
|
self.update_tree_view()
|
|
self.page.update()
|
|
|
|
def add_node(self, e=None):
|
|
"""ノード追加"""
|
|
if not self.name_field.value:
|
|
self.show_message("名前を入力してください", ft.Colors.RED)
|
|
return
|
|
|
|
new_node = ProductNode(
|
|
id=self.next_id,
|
|
name=self.name_field.value,
|
|
parent_id=self.selected_node.id if self.selected_node else None,
|
|
level=self.selected_node.level + 1 if self.selected_node else 0,
|
|
is_category=self.is_category_checkbox.value,
|
|
price=float(self.price_field.value) if not self.is_category_checkbox.value else 0.0,
|
|
stock=int(self.stock_field.value) if not self.is_category_checkbox.value else 0,
|
|
description=self.description_field.value
|
|
)
|
|
|
|
self.next_id += 1
|
|
|
|
if self.selected_node:
|
|
self.selected_node.add_child(new_node)
|
|
if not self.selected_node.expanded:
|
|
self.selected_node.expanded = True
|
|
else:
|
|
self.root_nodes.append(new_node)
|
|
|
|
self.update_tree_view()
|
|
self.clear_form()
|
|
self.save_data()
|
|
self.show_message("商品を追加しました", ft.Colors.GREEN)
|
|
|
|
def update_node(self, e=None):
|
|
"""ノード更新"""
|
|
if not self.selected_node or not self.name_field.value:
|
|
self.show_message("更新対象を選択してください", ft.Colors.RED)
|
|
return
|
|
|
|
self.selected_node.name = self.name_field.value
|
|
self.selected_node.is_category = self.is_category_checkbox.value
|
|
|
|
if not self.is_category_checkbox.value:
|
|
self.selected_node.price = float(self.price_field.value)
|
|
self.selected_node.stock = int(self.stock_field.value)
|
|
else:
|
|
self.selected_node.price = 0.0
|
|
self.selected_node.stock = 0
|
|
|
|
self.selected_node.description = self.description_field.value
|
|
|
|
self.update_tree_view()
|
|
self.save_data()
|
|
self.show_message("商品を更新しました", ft.Colors.GREEN)
|
|
|
|
def delete_node(self, e=None):
|
|
"""ノード削除"""
|
|
if not self.selected_node:
|
|
self.show_message("削除対象を選択してください", ft.Colors.RED)
|
|
return
|
|
|
|
# 子ノードも削除
|
|
def remove_from_parent(node: ProductNode, target: ProductNode):
|
|
if target in node.children:
|
|
node.remove_child(target.id)
|
|
return True
|
|
for child in node.children:
|
|
if remove_from_parent(child, target):
|
|
return True
|
|
return False
|
|
|
|
# 親を探して削除
|
|
removed = False
|
|
for root in self.root_nodes:
|
|
if root == self.selected_node:
|
|
self.root_nodes.remove(root)
|
|
removed = True
|
|
break
|
|
if remove_from_parent(root, self.selected_node):
|
|
removed = True
|
|
break
|
|
|
|
if removed:
|
|
self.selected_node = None
|
|
self.update_tree_view()
|
|
self.clear_form()
|
|
self.save_data()
|
|
self.show_message("商品を削除しました", ft.Colors.GREEN)
|
|
|
|
def clear_form(self):
|
|
"""フォームクリア"""
|
|
self.name_field.value = ""
|
|
self.price_field.value = "0"
|
|
self.stock_field.value = "0"
|
|
self.description_field.value = ""
|
|
self.is_category_checkbox.value = True
|
|
self.price_field.disabled = True
|
|
self.stock_field.disabled = True
|
|
|
|
def show_pdf_preview(self, e=None):
|
|
"""PDFプレビュー表示"""
|
|
self.preview_area.controls.clear()
|
|
|
|
# キャラクターベースの階層表示
|
|
preview_text = self._generate_character_preview()
|
|
|
|
self.preview_area.controls = [
|
|
ft.Container(
|
|
content=ft.Column([
|
|
ft.Text(
|
|
"商品マスタ一覧 - PDFプレビュー",
|
|
size=20,
|
|
weight=ft.FontWeight.BOLD,
|
|
text_align=ft.TextAlign.CENTER
|
|
),
|
|
ft.Divider(),
|
|
ft.Container(
|
|
content=ft.Text(
|
|
preview_text,
|
|
size=12,
|
|
font_family="Courier New"
|
|
),
|
|
padding=20,
|
|
bgcolor=ft.Colors.WHITE,
|
|
border=ft.Border.all(1, ft.Colors.GREY_300),
|
|
border_radius=5
|
|
)
|
|
]),
|
|
padding=20,
|
|
bgcolor=ft.Colors.GREY_50,
|
|
border_radius=10
|
|
)
|
|
]
|
|
|
|
self.preview_area.update()
|
|
|
|
def _generate_character_preview(self) -> str:
|
|
"""キャラクターベースのプレビュー生成"""
|
|
lines = []
|
|
lines.append("┌" + "─" * 60 + "┐")
|
|
lines.append("│" + "商品マスタ一覧".center(58, " ") + "│")
|
|
lines.append("├" + "─" * 60 + "┤")
|
|
|
|
def add_node_lines(node: ProductNode, prefix: str = "", is_last: bool = True):
|
|
# 現在のノード
|
|
connector = "└──" if is_last else "├──"
|
|
|
|
if node.is_category:
|
|
line = f"{prefix}{connector}【{node.name}】"
|
|
else:
|
|
line = f"{prefix}{connector}・{node.name} (¥{node.price:.0f}, 在庫:{node.stock})"
|
|
|
|
lines.append("│" + line.ljust(58, " ") + "│")
|
|
|
|
# 子ノード
|
|
if node.children:
|
|
child_prefix = prefix + (" " if is_last else "│ ")
|
|
for i, child in enumerate(node.children):
|
|
is_last_child = (i == len(node.children) - 1)
|
|
add_node_lines(child, child_prefix, is_last_child)
|
|
|
|
# 全ルートノードを追加
|
|
for i, root in enumerate(self.root_nodes):
|
|
is_last_root = (i == len(self.root_nodes) - 1)
|
|
add_node_lines(root, "", is_last_root)
|
|
|
|
lines.append("└" + "─" * 60 + "┘")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def show_message(self, message: str, color: ft.Colors):
|
|
"""メッセージ表示"""
|
|
self.page.snack_bar = ft.SnackBar(
|
|
content=ft.Text(message),
|
|
bgcolor=color
|
|
)
|
|
self.page.snack_bar.open = True
|
|
self.page.update()
|
|
|
|
def build(self) -> ft.Row:
|
|
"""UI構築"""
|
|
# 左側:ツリービュー
|
|
left_panel = ft.Container(
|
|
content=ft.Column([
|
|
ft.Text(
|
|
"商品階層構造",
|
|
size=18,
|
|
weight=ft.FontWeight.BOLD
|
|
),
|
|
ft.Divider(),
|
|
ft.Container(
|
|
content=self.tree_view,
|
|
border=ft.Border.all(1, ft.Colors.GREY_300),
|
|
border_radius=5,
|
|
padding=10,
|
|
height=400
|
|
)
|
|
]),
|
|
width=400,
|
|
padding=10
|
|
)
|
|
|
|
# 中央:入力フォーム
|
|
center_panel = ft.Container(
|
|
content=ft.Column([
|
|
ft.Text(
|
|
"商品情報編集",
|
|
size=18,
|
|
weight=ft.FontWeight.BOLD
|
|
),
|
|
ft.Divider(),
|
|
self.name_field,
|
|
ft.Row([
|
|
self.price_field,
|
|
self.stock_field
|
|
], spacing=10),
|
|
self.description_field,
|
|
self.is_category_checkbox,
|
|
ft.Divider(),
|
|
ft.Row([
|
|
self.add_btn,
|
|
self.update_btn,
|
|
self.delete_btn
|
|
], spacing=10)
|
|
]),
|
|
width=400,
|
|
padding=10
|
|
)
|
|
|
|
# 右側:プレビュー
|
|
right_panel = ft.Container(
|
|
content=ft.Column([
|
|
ft.Row([
|
|
ft.Text(
|
|
"PDFプレビュー",
|
|
size=18,
|
|
weight=ft.FontWeight.BOLD
|
|
),
|
|
self.preview_btn
|
|
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
|
|
ft.Divider(),
|
|
ft.Container(
|
|
content=self.preview_area,
|
|
border=ft.Border.all(1, ft.Colors.GREY_300),
|
|
border_radius=5,
|
|
padding=10,
|
|
height=400
|
|
)
|
|
]),
|
|
width=500,
|
|
padding=10
|
|
)
|
|
|
|
return ft.Row([
|
|
left_panel,
|
|
ft.VerticalDivider(width=1),
|
|
center_panel,
|
|
ft.VerticalDivider(width=1),
|
|
right_panel
|
|
], expand=True)
|
|
|
|
def create_hierarchical_product_master(page: ft.Page) -> HierarchicalProductMaster:
|
|
"""階層商品マスタ作成"""
|
|
master = HierarchicalProductMaster(page)
|
|
master.load_data()
|
|
return master
|