From a1e82e07142f3c83919098ec4881f35c8023b479 Mon Sep 17 00:00:00 2001 From: joe Date: Mon, 23 Feb 2026 22:06:25 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=88=E3=83=AB=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=81=AB=E6=96=B0=E8=A6=8F=E3=83=9C=E3=82=BF=E3=83=B3?= =?UTF-8?q?=E3=82=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 156 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 122 insertions(+), 34 deletions(-) diff --git a/main.py b/main.py index 144ed90..9950417 100644 --- a/main.py +++ b/main.py @@ -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