タイトルバーに新規ボタンを

This commit is contained in:
joe 2026-02-23 22:06:25 +09:00
parent 27fb3d7286
commit a1e82e0714

156
main.py
View file

@ -7,6 +7,8 @@ import flet as ft
import signal import signal
import sys import sys
import logging import logging
import asyncio
import threading
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from typing import List, Dict, Optional from typing import List, Dict, Optional
@ -72,7 +74,7 @@ class AppBar(ft.Container):
def __init__(self, title: str, show_back: bool = False, show_edit: bool = False, def __init__(self, title: str, show_back: bool = False, show_edit: bool = False,
on_back=None, on_edit=None, page=None, action_icon=None, action_tooltip: str = "編集", on_back=None, on_edit=None, page=None, action_icon=None, action_tooltip: str = "編集",
bottom: Optional[ft.Control] = None): bottom: Optional[ft.Control] = None, trailing_controls: Optional[list] = None):
super().__init__() super().__init__()
self.title = title self.title = title
self.show_back = show_back self.show_back = show_back
@ -83,6 +85,7 @@ class AppBar(ft.Container):
self.action_icon = action_icon or ft.Icons.EDIT self.action_icon = action_icon or ft.Icons.EDIT
self.action_tooltip = action_tooltip self.action_tooltip = action_tooltip
self.bottom = bottom self.bottom = bottom
self.trailing_controls = trailing_controls or []
self.bgcolor = ft.Colors.BLUE_GREY_50 self.bgcolor = ft.Colors.BLUE_GREY_50
self.padding = ft.Padding.symmetric(horizontal=16, vertical=8) self.padding = ft.Padding.symmetric(horizontal=16, vertical=8)
@ -130,6 +133,10 @@ class AppBar(ft.Container):
on_click=self.on_edit if self.on_edit else None on_click=self.on_edit if self.on_edit else None
) )
) )
elif self.trailing_controls:
controls.append(
ft.Row(self.trailing_controls, spacing=8, vertical_alignment=ft.CrossAxisAlignment.CENTER)
)
else: else:
controls.append(ft.Container(width=48)) # スペーサー controls.append(ft.Container(width=48)) # スペーサー
@ -162,6 +169,22 @@ class FlutterStyleDashboard:
self.is_customer_picker_open = False self.is_customer_picker_open = False
self.customer_search_query = "" self.customer_search_query = ""
self.show_offsets = False self.show_offsets = False
self._suppress_tap_after_long_press = False
self._toast_text = ft.Text("", color=ft.Colors.WHITE, size=12)
self._toast = ft.Container(
opacity=0,
visible=False,
animate_opacity=300,
content=ft.Container(
content=ft.Row([
ft.Icon(ft.Icons.INFO, size=16, color=ft.Colors.WHITE70),
self._toast_text,
], spacing=8, vertical_alignment=ft.CrossAxisAlignment.CENTER),
bgcolor=ft.Colors.BLUE_GREY_800,
padding=ft.Padding.symmetric(horizontal=12, vertical=8),
border_radius=8,
),
)
self.chain_verify_result = None self.chain_verify_result = None
self.is_new_customer_form_open = False self.is_new_customer_form_open = False
self.master_editor: Optional[UniversalMasterEditor] = None self.master_editor: Optional[UniversalMasterEditor] = None
@ -351,7 +374,18 @@ class FlutterStyleDashboard:
app_bar = AppBar( app_bar = AppBar(
title="伝票一覧", title="伝票一覧",
show_back=False, show_back=False,
show_edit=False show_edit=False,
trailing_controls=[
ft.Button(
content=ft.Row([
ft.Icon(ft.Icons.ADD),
ft.Text("新規伝票"),
], spacing=6),
on_click=lambda _: self.start_new_invoice(),
height=36,
style=ft.ButtonStyle(padding=ft.Padding.symmetric(horizontal=10, vertical=0)),
)
],
) )
logging.info("_build_invoice_list_screen: AppBar作成完了") logging.info("_build_invoice_list_screen: AppBar作成完了")
@ -361,11 +395,23 @@ class FlutterStyleDashboard:
result = ft.Column([ result = ft.Column([
app_bar, app_bar,
ft.Container(content=self._toast, padding=ft.Padding.symmetric(horizontal=16, vertical=4)),
ft.Container( ft.Container(
content=invoice_list, content=invoice_list,
expand=True, expand=True,
padding=ft.Padding.all(16) padding=ft.Padding.all(16)
), ),
# デバッグ用: スナック表示テストボタン(一覧画面下部)
ft.Container(
content=ft.Row([
ft.Button(
content=ft.Text("スナックテストを表示"),
on_click=lambda _: self._show_snack("テストスナック"),
),
ft.Text("コンソールに 'show_snack: テストスナック' が出ればハンドラは動作", size=12, color=ft.Colors.BLUE_GREY_600),
], spacing=12),
padding=ft.Padding.symmetric(horizontal=16, vertical=8),
),
], expand=True) ], expand=True)
logging.info("_build_invoice_list_screen: Column作成完了") logging.info("_build_invoice_list_screen: Column作成完了")
@ -377,6 +423,10 @@ class FlutterStyleDashboard:
if not self.editing_invoice: if not self.editing_invoice:
return ft.Column([ft.Text("伝票が選択されていません")]) return ft.Column([ft.Text("伝票が選択されていません")])
# is_edit_mode が立っている場合は強制的に編集モードにする
if getattr(self, 'is_edit_mode', False):
self.is_detail_edit_mode = True
is_locked = getattr(self.editing_invoice, 'final_locked', False) is_locked = getattr(self.editing_invoice, 'final_locked', False)
is_view_mode = not getattr(self, 'is_detail_edit_mode', False) is_view_mode = not getattr(self, 'is_detail_edit_mode', False)
app_bar = AppBar( app_bar = AppBar(
@ -394,6 +444,7 @@ class FlutterStyleDashboard:
return ft.Column([ return ft.Column([
app_bar, app_bar,
ft.Container(content=self._toast, padding=ft.Padding.symmetric(horizontal=16, vertical=4)),
body, body,
], expand=True) ], expand=True)
@ -715,6 +766,9 @@ class FlutterStyleDashboard:
display_amount = -abs(amount) if isinstance(slip, Invoice) and getattr(slip, "is_offset", False) else amount display_amount = -abs(amount) if isinstance(slip, Invoice) and getattr(slip, "is_offset", False) else amount
def on_single_tap(_): def on_single_tap(_):
if self._suppress_tap_after_long_press:
self._suppress_tap_after_long_press = False
return
if isinstance(slip, Invoice): if isinstance(slip, Invoice):
self.open_invoice_detail(slip) self.open_invoice_detail(slip)
@ -725,6 +779,7 @@ class FlutterStyleDashboard:
def on_long_press(_): def on_long_press(_):
# 長押しは直接編集画面へ(ビューワではなく編集) # 長押しは直接編集画面へ(ビューワではなく編集)
logging.info(f"long_press -> open_invoice_edit: {getattr(slip, 'invoice_number', 'unknown')}") logging.info(f"long_press -> open_invoice_edit: {getattr(slip, 'invoice_number', 'unknown')}")
self._suppress_tap_after_long_press = True
self.open_invoice_edit(slip) self.open_invoice_edit(slip)
show_offset_button = isinstance(slip, Invoice) and self.can_create_offset_invoice(slip) show_offset_button = isinstance(slip, Invoice) and self.can_create_offset_invoice(slip)
@ -967,49 +1022,50 @@ class FlutterStyleDashboard:
border_radius=8, border_radius=8,
) )
def open_invoice_edit(self, invoice: Invoice):
"""伝票編集画面を開く"""
self.editing_invoice = invoice
self.is_edit_mode = True
self.selected_customer = invoice.customer
self.selected_document_type = invoice.document_type
self.amount_value = str(invoice.items[0].unit_price if invoice.items else "0")
self.is_detail_edit_mode = True # 編集モードで開く
self.is_customer_picker_open = False
self.is_new_customer_form_open = False
self.current_tab = 1 # 詳細編集タブに切り替え
self.update_main_content()
def open_new_customer_form(self): def open_new_customer_form(self):
"""新規顧客フォームを開く(画面内遷移)""" """新規顧客フォームを開く(画面内遷移)"""
self.is_new_customer_form_open = True self.is_new_customer_form_open = True
self.update_main_content() self.update_main_content()
def start_new_invoice(self, _=None):
"""新規伝票作成ボタンから呼ばれる入口"""
self.editing_invoice = None
self._init_new_invoice()
def create_invoice_edit_screen(self) -> ft.Container: def create_invoice_edit_screen(self) -> ft.Container:
"""伝票編集画面(新規・編集統合)""" """伝票編集画面(新規・編集統合)"""
# 常に詳細編集画面を使用 # 常に詳細編集画面を使用
if not self.editing_invoice: if not self.editing_invoice:
# 新規伝票の場合は空のInvoiceを作成 self._init_new_invoice()
from models.invoice_models import Invoice, Customer, DocumentType
default_customer = Customer(
id=0,
name="選択してください",
formal_name="選択してください",
address="",
phone=""
)
self.editing_invoice = Invoice(
customer=default_customer,
date=datetime.now(),
items=[],
document_type=DocumentType.SALES,
invoice_number="NEW-" + str(int(datetime.now().timestamp())) # 新規伝票番号
)
self.is_detail_edit_mode = True # 新規作成モード
# 既存・新規共通で詳細編集画面を返す # 既存・新規共通で詳細編集画面を返す
return self._create_edit_existing_screen() return self._create_edit_existing_screen()
def _init_new_invoice(self):
"""新規伝票オブジェクトを準備"""
default_customer = Customer(
id=0,
name="選択してください",
formal_name="選択してください",
address="",
phone=""
)
self.editing_invoice = Invoice(
customer=default_customer,
date=datetime.now(),
items=[],
document_type=DocumentType.DRAFT,
invoice_number="NEW-" + str(int(datetime.now().timestamp()))
)
self.selected_customer = default_customer
self.selected_document_type = DocumentType.DRAFT
self.is_detail_edit_mode = True
self.is_edit_mode = True
self.is_customer_picker_open = False
self.is_new_customer_form_open = False
self.current_tab = 1
self.update_main_content()
def _create_edit_existing_screen(self) -> ft.Container: def _create_edit_existing_screen(self) -> ft.Container:
"""既存伝票の編集画面(新規・編集共通)""" """既存伝票の編集画面(新規・編集共通)"""
# 編集不可チェック新規作成時はFalse # 編集不可チェック新規作成時はFalse
@ -1286,6 +1342,8 @@ class FlutterStyleDashboard:
self._show_snack("伝票を更新しました", ft.Colors.GREEN_600) self._show_snack("伝票を更新しました", ft.Colors.GREEN_600)
# 一覧データは更新 # 一覧データは更新
self.invoices = self.app_service.invoice.get_recent_invoices(20) self.invoices = self.app_service.invoice.get_recent_invoices(20)
# 保存後の顧客選択状態を保持
self.selected_customer = getattr(self.editing_invoice, "customer", None)
# 設定により遷移先を変更 # 設定により遷移先を変更
if not self.stay_on_detail_after_save: if not self.stay_on_detail_after_save:
self.editing_invoice = None self.editing_invoice = None
@ -1627,16 +1685,41 @@ class FlutterStyleDashboard:
def _show_snack(self, message: str, color=ft.Colors.BLUE_GREY_800): def _show_snack(self, message: str, color=ft.Colors.BLUE_GREY_800):
try: try:
logging.info(f"show_snack: {message}")
snack = ft.SnackBar(content=ft.Text(message), bgcolor=color) snack = ft.SnackBar(content=ft.Text(message), bgcolor=color)
# attach to page to ensure it's rendered even if show_snack_bar is ignored
self.page.snack_bar = snack
# prefer show_snack_bar API if available (more reliable on web) # prefer show_snack_bar API if available (more reliable on web)
if hasattr(self.page, "show_snack_bar"): if hasattr(self.page, "show_snack_bar"):
self.page.show_snack_bar(snack) self.page.show_snack_bar(snack)
else: else:
self.page.snack_bar = snack
self.page.snack_bar.open = True self.page.snack_bar.open = True
self.page.update() # ensure open flag is set for both paths
if hasattr(self.page, "snack_bar"):
self.page.snack_bar.open = True
# in-app toast fallback (必ず画面に表示される)
if self._toast is not None:
self._toast_text.value = message
self._toast.content.bgcolor = color if color else ft.Colors.BLUE_GREY_800
self._toast.visible = True
self._toast.opacity = 1
try:
loop = asyncio.get_event_loop()
loop.call_later(3, self._hide_toast)
except RuntimeError:
threading.Timer(3, self._hide_toast).start()
self.page.update()
except Exception as e: except Exception as e:
logging.warning(f"snack_bar表示失敗: {e}") logging.warning(f"snack_bar表示失敗: {e}")
def _hide_toast(self):
try:
if self._toast is not None:
self._toast.opacity = 0
self._toast.visible = False
self.page.update()
except Exception as e:
logging.warning(f"toast hide failed: {e}")
def create_new_customer_screen(self) -> ft.Container: def create_new_customer_screen(self) -> ft.Container:
"""新規/既存顧客登録・編集画面""" """新規/既存顧客登録・編集画面"""
editing_customer = getattr(self, "editing_customer_for_form", None) editing_customer = getattr(self, "editing_customer_for_form", None)
@ -1667,6 +1750,11 @@ class FlutterStyleDashboard:
self.selected_customer = editing_customer self.selected_customer = editing_customer
if self.editing_invoice: if self.editing_invoice:
self.editing_invoice.customer = editing_customer self.editing_invoice.customer = editing_customer
# 既存伝票一覧も更新しておく(顧客名がすぐ反映されるように)
try:
self.invoices = self.app_service.invoice.get_recent_invoices(20)
except Exception:
pass
self._show_snack("顧客を更新しました", ft.Colors.GREEN_600) self._show_snack("顧客を更新しました", ft.Colors.GREEN_600)
self.editing_customer_for_form = None self.editing_customer_for_form = None
self.is_customer_picker_open = False self.is_customer_picker_open = False