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

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 sys
import logging
import asyncio
import threading
import sqlite3
from datetime import datetime
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,
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__()
self.title = title
self.show_back = show_back
@ -83,6 +85,7 @@ class AppBar(ft.Container):
self.action_icon = action_icon or ft.Icons.EDIT
self.action_tooltip = action_tooltip
self.bottom = bottom
self.trailing_controls = trailing_controls or []
self.bgcolor = ft.Colors.BLUE_GREY_50
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
)
)
elif self.trailing_controls:
controls.append(
ft.Row(self.trailing_controls, spacing=8, vertical_alignment=ft.CrossAxisAlignment.CENTER)
)
else:
controls.append(ft.Container(width=48)) # スペーサー
@ -162,6 +169,22 @@ class FlutterStyleDashboard:
self.is_customer_picker_open = False
self.customer_search_query = ""
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.is_new_customer_form_open = False
self.master_editor: Optional[UniversalMasterEditor] = None
@ -351,7 +374,18 @@ class FlutterStyleDashboard:
app_bar = AppBar(
title="伝票一覧",
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作成完了")
@ -361,11 +395,23 @@ class FlutterStyleDashboard:
result = ft.Column([
app_bar,
ft.Container(content=self._toast, padding=ft.Padding.symmetric(horizontal=16, vertical=4)),
ft.Container(
content=invoice_list,
expand=True,
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)
logging.info("_build_invoice_list_screen: Column作成完了")
@ -377,6 +423,10 @@ class FlutterStyleDashboard:
if not self.editing_invoice:
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_view_mode = not getattr(self, 'is_detail_edit_mode', False)
app_bar = AppBar(
@ -394,6 +444,7 @@ class FlutterStyleDashboard:
return ft.Column([
app_bar,
ft.Container(content=self._toast, padding=ft.Padding.symmetric(horizontal=16, vertical=4)),
body,
], expand=True)
@ -715,6 +766,9 @@ class FlutterStyleDashboard:
display_amount = -abs(amount) if isinstance(slip, Invoice) and getattr(slip, "is_offset", False) else amount
def on_single_tap(_):
if self._suppress_tap_after_long_press:
self._suppress_tap_after_long_press = False
return
if isinstance(slip, Invoice):
self.open_invoice_detail(slip)
@ -725,6 +779,7 @@ class FlutterStyleDashboard:
def on_long_press(_):
# 長押しは直接編集画面へ(ビューワではなく編集)
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)
show_offset_button = isinstance(slip, Invoice) and self.can_create_offset_invoice(slip)
@ -967,48 +1022,49 @@ class FlutterStyleDashboard:
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):
"""新規顧客フォームを開く(画面内遷移)"""
self.is_new_customer_form_open = True
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:
"""伝票編集画面(新規・編集統合)"""
# 常に詳細編集画面を使用
if not self.editing_invoice:
# 新規伝票の場合は空の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 # 新規作成モード
self._init_new_invoice()
# 既存・新規共通で詳細編集画面を返す
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:
"""既存伝票の編集画面(新規・編集共通)"""
@ -1286,6 +1342,8 @@ class FlutterStyleDashboard:
self._show_snack("伝票を更新しました", ft.Colors.GREEN_600)
# 一覧データは更新
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:
self.editing_invoice = None
@ -1627,16 +1685,41 @@ class FlutterStyleDashboard:
def _show_snack(self, message: str, color=ft.Colors.BLUE_GREY_800):
try:
logging.info(f"show_snack: {message}")
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)
if hasattr(self.page, "show_snack_bar"):
self.page.show_snack_bar(snack)
else:
self.page.snack_bar = snack
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:
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:
"""新規/既存顧客登録・編集画面"""
editing_customer = getattr(self, "editing_customer_for_form", None)
@ -1667,6 +1750,11 @@ class FlutterStyleDashboard:
self.selected_customer = editing_customer
if self.editing_invoice:
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.editing_customer_for_form = None
self.is_customer_picker_open = False