h-1.flet.3/app_slip_interactive.py
2026-02-20 23:24:01 +09:00

412 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
インタラクティブ伝票ビューア
Flutter参考プロジェクトの構造を適用
"""
import flet as ft
import sqlite3
import signal
import sys
import logging
from datetime import datetime
from components.pinch_handler import PinchHandler
from models.invoice_models import DocumentType, Invoice, create_sample_invoices
# カラーテーマ定義
DARK_THEME = {
'background': ft.Colors.GREY_900,
'card_bg': ft.Colors.GREY_800,
'text_primary': ft.Colors.WHITE,
'text_secondary': ft.Colors.GREY_300,
'accent': ft.Colors.BLUE_400
}
LIGHT_THEME = {
'background': ft.Colors.BLUE_50,
'card_bg': ft.Colors.WHITE,
'text_primary': ft.Colors.BLACK,
'text_secondary': ft.Colors.GREY_700,
'accent': ft.Colors.BLUE_600
}
class InteractiveSlipViewer:
def __init__(self, page: ft.Page):
self.page = page
# 状態管理を先に初期化
self.slip_data = []
self.test_mode = False # テストモード
self.test_logs = [] # 操作ログ
self.is_dark_theme = False # テーマ設定
# ページ設定
self.setup_page()
# データベース設定
self.setup_database()
# ピンチハンドラー初期化
self.pinch_handler = PinchHandler(page)
self.pinch_handler.set_callbacks(
on_zoom_change=self.on_zoom_change,
on_tap=self.on_slip_tap,
on_double_tap=self.on_slip_double_tap,
on_long_press=self.on_slip_long_press
)
# UI初期化
self.setup_ui()
def setup_page(self):
self.page.title = "インタラクティブ伝票ビューア"
self.page.window.width = 420
self.page.window.height = 900
self.page.window.resizable = False
self.page.window_center = True
# キーボードイベントハンドラー
self.page.on_keyboard_event = self.on_keyboard_event
def setup_database(self):
try:
self.conn = sqlite3.connect('sales_assist.db')
self.cursor = self.conn.cursor()
# 伝票テーブル作成
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS slips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slip_type TEXT,
customer_name TEXT,
amount REAL,
date TEXT,
status TEXT,
description TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
# サンプルデータ
self.create_sample_data()
except Exception as e:
logging.error(f"DBエラー: {e}")
self.conn = None
def create_sample_data(self):
"""サンプル伝票データ作成"""
try:
# Flutterモデルからサンプルデータを生成
sample_invoices = create_sample_invoices()
for invoice in sample_invoices:
self.cursor.execute('''
INSERT OR REPLACE INTO slips
(slip_type, customer_name, amount, date, status, description)
VALUES (?, ?, ?, ?, ?, ?)
''', (
invoice.document_type.value,
invoice.customer.formal_name,
invoice.total_amount,
invoice.date.strftime('%Y-%m-%d'),
'完了',
invoice.notes
))
self.conn.commit()
except Exception as e:
logging.error(f"サンプルデータ作成エラー: {e}")
def setup_ui(self):
# テーマ設定
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
# ヘッダー
header = ft.Container(
content=ft.Row([
ft.Text("📋 インタラクティブ伝票", size=18, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Container(expand=True),
ft.IconButton(ft.Icons.SEARCH, icon_size=16, icon_color=theme['text_primary']),
ft.Button(
"テスト",
bgcolor=theme['accent'],
color=theme['text_primary'],
on_click=self.toggle_test_mode
)
]),
padding=15,
bgcolor=theme['accent'],
border_radius=15,
margin=ft.Margin.only(bottom=10)
)
# 伝票グリッド
self.slip_grid = ft.GridView(
runs_count=2,
spacing=10,
run_spacing=10,
child_aspect_ratio=1.2,
padding=ft.Padding.symmetric(horizontal=15, vertical=10)
)
# 詳細パネル
self.detail_panel = ft.Container(
content=ft.Text("詳細パネル"),
visible=False,
padding=15,
bgcolor=theme['card_bg'],
border_radius=10,
shadow=ft.BoxShadow(
spread_radius=1,
blur_radius=5,
color=ft.Colors.with_opacity(0.2, ft.Colors.BLACK),
offset=ft.Offset(0, 2)
)
)
# テストモードパネル
self.test_panel = ft.Container(
content=ft.Column([
ft.Text("🧪 テストモード", size=14, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Divider(height=1),
ft.Text("キーボード操作:", size=12, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Text("+ : ズームイン", size=10, color=theme['text_secondary']),
ft.Text("- : ズームアウト", size=10, color=theme['text_secondary']),
ft.Text("R : ズームリセット", size=10, color=theme['text_secondary']),
ft.Text(f"現在のズーム: {int(self.pinch_handler.zoom_level * 100)}%", size=10, color=theme['text_primary']),
ft.Text("操作履歴:", size=12, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Column([], spacing=2)
], spacing=5),
visible=False,
padding=10,
bgcolor=theme['card_bg'],
border_radius=10,
margin=ft.Margin.only(bottom=10)
)
# メインコンテナ
main_container = ft.Column([
self.slip_grid,
self.test_panel,
self.detail_panel
], scroll=ft.ScrollMode.AUTO)
# ページに追加
self.page.add(
ft.Column([
header,
ft.Container(
content=main_container,
expand=True,
bgcolor=theme['background']
)
])
)
# 初期データ読み込み
self.load_slips()
def load_slips(self):
"""伝票データ読み込み"""
if not self.conn:
logging.error("データベース接続がありません")
return
try:
self.cursor.execute("SELECT * FROM slips ORDER BY date DESC")
rows = self.cursor.fetchall()
# タプルからInvoiceオブジェクトに変換
self.slip_data = []
for row in rows:
# 簡単なInvoiceオブジェクトを作成復元用
from models.invoice_models import Customer, InvoiceItem, DocumentType
customer = Customer(1, row[2], row[2]) # id, name, formal_name
items = [InvoiceItem("サンプル明細", 1, int(row[3]))] # description, quantity, unit_price
# DocumentTypeの文字列からEnumに変換
doc_type_str = row[1]
doc_type = next((dt for dt in DocumentType if dt.value == doc_type_str), DocumentType.INVOICE)
invoice = Invoice(
customer=customer,
date=datetime.strptime(row[4], '%Y-%m-%d'),
items=items,
document_type=doc_type
)
self.slip_data.append(invoice)
self.update_slip_grid()
except Exception as e:
logging.error(f"伝票読み込みエラー: {e}")
self.slip_data = []
def update_slip_grid(self):
"""伝票グリッド更新"""
self.slip_grid.controls.clear()
for slip in self.slip_data:
slip_card = self.create_slip_card(slip)
self.slip_grid.controls.append(slip_card)
self.page.update()
def create_slip_card(self, invoice: Invoice) -> ft.Container:
"""伝票カード作成"""
# テーマ取得
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
# ドキュメントタイプに応じたアイコンと色
type_config = {
DocumentType.SALES: {"icon": "💰", "color": ft.Colors.GREEN},
DocumentType.ESTIMATE: {"icon": "📄", "color": ft.Colors.BLUE},
DocumentType.DELIVERY: {"icon": "📦", "color": ft.Colors.PURPLE},
DocumentType.INVOICE: {"icon": "📋", "color": ft.Colors.ORANGE},
DocumentType.RECEIPT: {"icon": "🧾", "color": ft.Colors.RED}
}
config = type_config.get(invoice.document_type, {"icon": "📝", "color": ft.Colors.GREY})
# 通常のカード作成
card = ft.Container(
content=ft.Column([
ft.Container(
content=ft.Text(config["icon"], size=24),
width=40,
height=40,
bgcolor=config["color"],
border_radius=20,
alignment=ft.alignment.Alignment(0, 0)
),
ft.Text(invoice.document_type.value, size=12, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Text(f"{invoice.customer.formal_name} ¥{invoice.total_amount:,}", size=10, color=theme['text_secondary']),
], spacing=5, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
padding=10,
bgcolor=theme['card_bg'],
border_radius=10,
shadow=ft.BoxShadow(
spread_radius=1,
blur_radius=5,
color=ft.Colors.with_opacity(0.2, ft.Colors.BLACK),
offset=ft.Offset(0, 2)
)
)
return ft.GestureDetector(
content=card,
on_tap=lambda _: self.on_slip_tap(invoice)
)
def on_zoom_change(self, zoom_level: float):
"""ズームレベル変更時"""
# ズーム機能は一時的に無効化
pass
def on_slip_tap(self, invoice: Invoice):
"""伝票タップ"""
self.show_slip_detail(invoice)
def on_slip_double_tap(self, invoice: Invoice):
"""伝票ダブルタップ"""
self.show_slip_detail(invoice)
def on_slip_long_press(self, invoice: Invoice):
"""伝票ロングプレス"""
self.show_slip_detail(invoice)
def show_slip_detail(self, invoice: Invoice):
"""伝票詳細表示"""
# テーマ取得
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
self.detail_panel.content = ft.Column([
ft.Row([
ft.Text("伝票詳細", size=16, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.IconButton(ft.Icons.CLOSE, on_click=self.hide_detail, icon_color=theme['text_primary'])
]),
ft.Divider(),
ft.Text(f"種類: {invoice.document_type.value}", size=14, color=theme['text_primary']),
ft.Text(f"顧客: {invoice.customer.formal_name}", size=14, color=theme['text_primary']),
ft.Text(f"金額: ¥{invoice.total_amount:,}", size=14, color=theme['text_primary']),
ft.Text(f"日付: {invoice.date.strftime('%Y/%m/%d')}", size=14, color=theme['text_primary']),
ft.Text(f"請求書番号: {invoice.invoice_number}", size=14, color=theme['text_primary']),
ft.Text(f"説明: {invoice.notes or 'なし'}", size=12, color=theme['text_secondary']),
ft.Row([
ft.Button("編集", bgcolor=theme['accent'], color=theme['text_primary']),
ft.Button("削除", bgcolor=ft.Colors.RED, color=theme['text_primary']),
], spacing=10)
], spacing=10)
self.detail_panel.visible = True
self.page.update()
def on_keyboard_event(self, e: ft.KeyboardEvent):
"""キーボードイベント処理"""
if self.test_mode:
if e.key == "+":
self.pinch_handler.zoom_in()
self.add_test_log("キーボード: + (ズームイン)")
elif e.key == "-":
self.pinch_handler.zoom_out()
self.add_test_log("キーボード: - (ズームアウト)")
elif e.key == "r":
self.pinch_handler.reset_zoom()
self.add_test_log("キーボード: R (ズームリセット)")
elif e.key == " ": # Spaceキー
self.add_test_log("キーボード: Space (ダブルタップ代替)")
elif e.key == "t": # Tキー
self.toggle_test_mode()
def on_mouse_event(self, e):
"""マウスイベント処理"""
if self.test_mode:
if hasattr(e, 'button'):
if e.button == "right":
self.pinch_handler.zoom_in()
self.add_test_log(f"マウス: 右クリック (ズームイン)")
elif e.button == "middle":
self.pinch_handler.zoom_out()
self.add_test_log(f"マウス: 中クリック (ズームアウト)")
def toggle_test_mode(self):
"""テストモード切替"""
self.test_mode = not self.test_mode
self.test_panel.visible = self.test_mode
self.page.update()
def add_test_log(self, message: str):
"""テストログ追加"""
if hasattr(self, 'test_logs'):
self.test_logs.append(message)
else:
self.test_logs = [message]
# ログ表示更新
if len(self.test_logs) > 10: # 最新10件のみ表示
self.test_logs = self.test_logs[-10:]
# テーマ取得
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
log_container = ft.Column([
ft.Text(log, size=8, color=theme['text_secondary'])
for log in self.test_logs
])
self.test_panel.content.controls[-1].controls = [log_container]
self.page.update()
def hide_detail(self, e=None):
"""詳細パネル非表示"""
self.detail_panel.visible = False
self.page.update()
def main(page: ft.Page):
try:
viewer = InteractiveSlipViewer(page)
logging.info("インタラクティブ伝票ビューア起動")
except Exception as e:
logging.error(f"起動エラー: {e}")
if __name__ == "__main__":
ft.run(main)