333 lines
12 KiB
Python
333 lines
12 KiB
Python
"""
|
||
販売アシスト・ダッシュボード(テンプレート)
|
||
統合ハブとして全機能へのアクセスを提供
|
||
"""
|
||
|
||
import flet as ft
|
||
import sqlite3
|
||
import signal
|
||
import sys
|
||
import logging
|
||
from datetime import datetime
|
||
from typing import List, Dict, Optional
|
||
|
||
# ロギング設定
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||
)
|
||
|
||
class DashboardTemplate:
|
||
"""ダッシュボードテンプレート"""
|
||
|
||
def __init__(self, page: ft.Page):
|
||
self.page = page
|
||
self.setup_page()
|
||
self.setup_database()
|
||
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.theme_mode = ft.ThemeMode.LIGHT
|
||
|
||
# シグナルハンドラ
|
||
def signal_handler(signum, frame):
|
||
logging.info("アプリケーション正常終了")
|
||
sys.exit(0)
|
||
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
|
||
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 sales_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
date TEXT,
|
||
amount REAL,
|
||
product TEXT,
|
||
customer TEXT
|
||
)
|
||
''')
|
||
|
||
self.conn.commit()
|
||
logging.info("データベース接続完了")
|
||
|
||
except Exception as e:
|
||
logging.error(f"データベースエラー: {e}")
|
||
self.conn = None
|
||
|
||
def setup_ui(self):
|
||
"""UI構築"""
|
||
# ヘッダー
|
||
header = ft.Container(
|
||
content=ft.Column([
|
||
ft.Text(
|
||
"🏠 販売アシスト",
|
||
size=24,
|
||
weight=ft.FontWeight.BOLD,
|
||
color=ft.Colors.WHITE,
|
||
text_align=ft.TextAlign.CENTER
|
||
),
|
||
ft.Text(
|
||
f"今日: {datetime.now().strftime('%m/%d %H:%M')}",
|
||
size=14,
|
||
color=ft.Colors.WHITE,
|
||
text_align=ft.TextAlign.CENTER
|
||
)
|
||
], spacing=5),
|
||
padding=20,
|
||
bgcolor=ft.Colors.BLUE_600,
|
||
border_radius=15,
|
||
margin=ft.Margin.only(bottom=20)
|
||
)
|
||
|
||
# ツールバー(コンパクト)
|
||
toolbar = ft.Container(
|
||
content=ft.Row([
|
||
ft.IconButton(ft.Icons.ADD, tooltip="新規下書き", icon_size=20),
|
||
ft.IconButton(ft.Icons.SEARCH, tooltip="検索", icon_size=20),
|
||
ft.IconButton(ft.Icons.FILTER_LIST, tooltip="フィルター", icon_size=20),
|
||
ft.Container(expand=True),
|
||
ft.IconButton(ft.Icons.SYNC, tooltip="更新", icon_size=20),
|
||
], spacing=5),
|
||
padding=10,
|
||
bgcolor=ft.Colors.BLUE_50
|
||
)
|
||
|
||
# 検索バー
|
||
search_bar = ft.Container(
|
||
content=ft.TextField(
|
||
hint_text="下書き・顧客・商品を検索...",
|
||
prefix_icon=ft.Icons.SEARCH,
|
||
filled=True,
|
||
dense=True
|
||
),
|
||
padding=ft.Padding.symmetric(horizontal=15, vertical=5)
|
||
)
|
||
|
||
# メイン機能グリッド(参考デザイン風)
|
||
main_grid = ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("🏠 メニュー", size=16, weight=ft.FontWeight.BOLD),
|
||
ft.GridView(
|
||
runs_count=2, # 2列グリッド
|
||
spacing=10,
|
||
run_spacing=10,
|
||
controls=[
|
||
self.create_grid_card("💰", "売上", "売上管理", ft.Colors.GREEN, "app_simple_working.py"),
|
||
self.create_grid_card("👥", "顧客", "顧客管理", ft.Colors.ORANGE, "app_master_management.py"),
|
||
self.create_grid_card("📦", "商品", "商品管理", ft.Colors.PURPLE, "app_hierarchical_product_master.py"),
|
||
self.create_grid_card("📋", "伝票", "伝票管理", ft.Colors.RED, "app_slip_framework_demo.py"),
|
||
self.create_grid_card("🔍", "検索", "伝票検索", ft.Colors.BLUE, "app_slip_explorer.py"),
|
||
self.create_grid_card("📱", "操作", "インタラクティブ", ft.Colors.CYAN, "app_slip_interactive.py"),
|
||
]
|
||
)
|
||
], spacing=10),
|
||
padding=15,
|
||
bgcolor=ft.Colors.WHITE,
|
||
border_radius=15,
|
||
shadow=ft.BoxShadow(
|
||
spread_radius=1,
|
||
blur_radius=5,
|
||
color=ft.Colors.GREY_300,
|
||
offset=ft.Offset(0, 2)
|
||
),
|
||
margin=ft.Margin.only(bottom=15)
|
||
)
|
||
|
||
# 下書き一覧(コンパクト)
|
||
draft_section = ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("📝 最近の下書き", size=16, weight=ft.FontWeight.BOLD),
|
||
ft.Divider(height=1),
|
||
self.create_draft_item("売上メモ", "顧客: 田中様", "2分前"),
|
||
self.create_draft_item("商品リスト", "新商品10件", "1時間前"),
|
||
ft.Container(
|
||
content=ft.Button("+ もっと見る", bgcolor=ft.Colors.BLUE_50, color=ft.Colors.BLUE_700),
|
||
alignment=ft.alignment.Alignment(0, 0)
|
||
)
|
||
], spacing=8),
|
||
padding=15,
|
||
bgcolor=ft.Colors.WHITE,
|
||
border_radius=15,
|
||
margin=ft.Margin.only(bottom=15)
|
||
)
|
||
|
||
# 設定セクション
|
||
settings_section = ft.Container(
|
||
content=ft.Row([
|
||
ft.IconButton(ft.Icons.SETTINGS, icon_size=20),
|
||
ft.Text("設定", size=14),
|
||
ft.Container(expand=True),
|
||
ft.Icon(ft.Icons.CHEVRON_RIGHT, size=16, color=ft.Colors.GREY_400)
|
||
]),
|
||
padding=15,
|
||
bgcolor=ft.Colors.WHITE,
|
||
border_radius=10,
|
||
border=ft.Border.all(1, ft.Colors.GREY_200)
|
||
)
|
||
|
||
# 状態サマリー
|
||
status_summary = ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("📈 状態サマリー", size=18, weight=ft.FontWeight.BOLD),
|
||
ft.Divider(height=1, thickness=1),
|
||
self.get_status_info()
|
||
], spacing=10),
|
||
padding=20,
|
||
bgcolor=ft.Colors.BLUE_50,
|
||
border_radius=15,
|
||
margin=ft.Margin.only(bottom=20)
|
||
)
|
||
|
||
# Debug情報
|
||
debug_info = ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("🔧 Debug情報", size=16, weight=ft.FontWeight.BOLD),
|
||
ft.Text(f"DB接続: {'✅' if self.conn else '❌'}", size=12),
|
||
ft.Text(f"ウィンドウ: 420x900", size=12),
|
||
ft.Text(f"テーマ: ライト", size=12),
|
||
], spacing=5),
|
||
padding=15,
|
||
bgcolor=ft.Colors.GREY_100,
|
||
border_radius=10
|
||
)
|
||
|
||
# メインコンテナ(再構成)
|
||
self.main_container = ft.Column([
|
||
header,
|
||
search_bar,
|
||
main_grid,
|
||
draft_section,
|
||
settings_section
|
||
], spacing=5, scroll=ft.ScrollMode.AUTO)
|
||
|
||
# ページに追加
|
||
self.page.add(
|
||
ft.Container(
|
||
content=self.main_container,
|
||
padding=10, # 20 → 10に縮小
|
||
bgcolor=ft.Colors.GREY_50,
|
||
expand=True
|
||
)
|
||
)
|
||
|
||
def create_grid_card(self, icon: str, title: str, subtitle: str, color: ft.Colors, app_file: str) -> ft.Container:
|
||
"""グリッドカード作成(参考デザイン風)"""
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
ft.Container(
|
||
content=ft.Text(icon, size=32),
|
||
width=60,
|
||
height=60,
|
||
bgcolor=color,
|
||
alignment=ft.alignment.Alignment(0, 0),
|
||
border_radius=15,
|
||
margin=ft.Margin.only(bottom=10)
|
||
),
|
||
ft.Text(title, size=14, weight=ft.FontWeight.BOLD, text_align=ft.TextAlign.CENTER),
|
||
ft.Text(subtitle, size=10, color=ft.Colors.GREY_600, text_align=ft.TextAlign.CENTER)
|
||
], spacing=5),
|
||
padding=15,
|
||
bgcolor=ft.Colors.WHITE,
|
||
border_radius=15,
|
||
border=ft.Border.all(1, ft.Colors.GREY_200),
|
||
on_click=lambda _: self.launch_app(app_file, title)
|
||
)
|
||
|
||
def create_draft_item(self, title: str, description: str, time: str) -> ft.Container:
|
||
"""下書きアイテム作成"""
|
||
return ft.Container(
|
||
content=ft.Row([
|
||
ft.Column([
|
||
ft.Text(title, size=14, weight=ft.FontWeight.BOLD),
|
||
ft.Text(description, size=12, color=ft.Colors.GREY_600)
|
||
], expand=True),
|
||
ft.Text(time, size=10, color=ft.Colors.GREY_500)
|
||
], spacing=10),
|
||
padding=10,
|
||
bgcolor=ft.Colors.GREY_50,
|
||
border_radius=8,
|
||
border=ft.Border.all(1, ft.Colors.GREY_200)
|
||
)
|
||
|
||
def get_status_info(self) -> ft.Column:
|
||
"""状態情報取得"""
|
||
if not self.conn:
|
||
return ft.Column([
|
||
ft.Text("データベース未接続", size=12, color=ft.Colors.RED),
|
||
ft.Text("機能制限中", size=12, color=ft.Colors.ORANGE)
|
||
], spacing=5)
|
||
|
||
try:
|
||
# 今日の売上件数
|
||
today = datetime.now().strftime('%Y-%m-%d')
|
||
self.cursor.execute("SELECT COUNT(*) FROM sales_log WHERE date = ?", (today,))
|
||
today_sales = self.cursor.fetchone()[0]
|
||
|
||
return ft.Column([
|
||
ft.Text(f"今日の売上: {today_sales}件", size=14, color=ft.Colors.BLUE_700),
|
||
ft.Text("システム状態: 正常", size=14, color=ft.Colors.GREEN_600),
|
||
ft.Text("最終更新: 剛剣", size=12, color=ft.Colors.GREY_600)
|
||
], spacing=5)
|
||
|
||
except Exception as e:
|
||
return ft.Column([
|
||
ft.Text("状態取得エラー", size=12, color=ft.Colors.RED),
|
||
ft.Text(str(e), size=10, color=ft.Colors.GREY_600)
|
||
], spacing=5)
|
||
|
||
def launch_app(self, app_file: str, app_name: str):
|
||
"""アプリ起動"""
|
||
import subprocess
|
||
|
||
try:
|
||
# SnackBarで通知
|
||
self.page.snack_bar = ft.SnackBar(
|
||
content=ft.Text(f"{app_name}を起動中..."),
|
||
bgcolor=ft.Colors.BLUE
|
||
)
|
||
self.page.snack_bar.open = True
|
||
self.page.update()
|
||
|
||
# サブプロセスで起動
|
||
subprocess.Popen(['python', app_file])
|
||
logging.info(f"{app_name}起動: {app_file}")
|
||
|
||
except Exception as e:
|
||
logging.error(f"{app_name}起動エラー: {e}")
|
||
self.page.snack_bar = ft.SnackBar(
|
||
content=ft.Text(f"起動エラー: {e}"),
|
||
bgcolor=ft.Colors.RED
|
||
)
|
||
self.page.snack_bar.open = True
|
||
self.page.update()
|
||
|
||
def main(page: ft.Page):
|
||
"""メイン関数"""
|
||
try:
|
||
dashboard = DashboardTemplate(page)
|
||
logging.info("ダッシュボード起動完了")
|
||
|
||
except Exception as e:
|
||
logging.error(f"ダッシュボード起動エラー: {e}")
|
||
page.snack_bar = ft.SnackBar(
|
||
content=ft.Text(f"起動エラー: {e}"),
|
||
bgcolor=ft.Colors.RED
|
||
)
|
||
page.snack_bar.open = True
|
||
page.update()
|
||
|
||
if __name__ == "__main__":
|
||
ft.run(main)
|