"""編集系画面で再利用するEditorフレームワーク(最小版)。""" from dataclasses import dataclass from typing import Callable, List, Optional import flet as ft from models.invoice_models import InvoiceItem, Product @dataclass class ValidationResult: """編集内容の検証結果。""" ok: bool errors: List[str] def normalize_invoice_items(items: List[InvoiceItem]) -> List[InvoiceItem]: """保存前に明細を正規化(空行除去・数値補正)。""" normalized: List[InvoiceItem] = [] for item in items: description = (item.description or "").strip() quantity = int(item.quantity or 0) unit_price = int(item.unit_price or 0) if not description and quantity == 0 and unit_price == 0: continue if quantity <= 0: quantity = 1 normalized.append( InvoiceItem( description=description, quantity=quantity, unit_price=unit_price, is_discount=bool(getattr(item, "is_discount", False)), product_id=getattr(item, "product_id", None), ) ) return normalized def validate_invoice_items(items: List[InvoiceItem]) -> ValidationResult: """明細の最小バリデーション。""" errors: List[str] = [] if not items: errors.append("明細を1行以上入力してください") return ValidationResult(ok=False, errors=errors) for idx, item in enumerate(items, start=1): if not (item.description or "").strip(): errors.append(f"{idx}行目: 商品名を入力してください") if int(item.quantity or 0) <= 0: errors.append(f"{idx}行目: 数量は1以上で入力してください") if int(item.unit_price or 0) < 0: errors.append(f"{idx}行目: 単価は0以上で入力してください") return ValidationResult(ok=len(errors) == 0, errors=errors) def build_invoice_items_view_table(items: List[InvoiceItem]) -> ft.Column: """読み取り専用の明細テーブル。""" header_row = ft.Row( [ ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), ft.Container(width=35), ] ) data_rows = [] for item in items: data_rows.append( ft.Row( [ ft.Text(item.description, size=12, expand=True), ft.Text(str(item.quantity), size=12, width=35, text_align=ft.TextAlign.RIGHT), ft.Text(f"¥{item.unit_price:,}", size=12, width=70, text_align=ft.TextAlign.RIGHT), ft.Text( f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT, ), ft.Container(width=35), ] ) ) list_control = ft.Column(data_rows, spacing=4) return ft.Column( [ header_row, ft.Divider(height=1, color=ft.Colors.GREY_400), ft.Container(content=list_control, height=250, padding=0), ] ) def build_invoice_items_edit_table( items: List[InvoiceItem], is_locked: bool, on_update_field: Callable[[int, str, str], None], on_delete_row: Callable[[int], None], products: List[Product], on_product_select: Callable[[int], None] | None = None, row_refs: Optional[dict] = None, ) -> ft.Column: """編集モードの明細テーブル。""" header_row = ft.Row( [ ft.Text("商品", size=12, weight=ft.FontWeight.BOLD, width=180), ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), ft.Container(width=35), ] ) data_rows: List[ft.Control] = [] for i, item in enumerate(items): product_label = item.description if item.description else "商品選択" product_field = ft.Button( content=ft.Text(product_label, no_wrap=True), width=180, height=36, on_click=(lambda _, idx=i: on_product_select(idx)) if on_product_select else None, disabled=is_locked, style=ft.ButtonStyle( padding=ft.Padding.symmetric(horizontal=10, vertical=6), bgcolor=ft.Colors.BLUE_GREY_100, shape=ft.RoundedRectangleBorder(radius=4), ), ) quantity_field = ft.TextField( value=str(item.quantity), text_size=12, height=28, width=35, text_align=ft.TextAlign.RIGHT, border=ft.border.all(1, ft.Colors.BLUE_200), bgcolor=ft.Colors.WHITE, content_padding=ft.padding.all(5), on_change=lambda e, idx=i: on_update_field(idx, "quantity", e.control.value), keyboard_type=ft.KeyboardType.NUMBER, ) unit_price_field = ft.TextField( value=f"{item.unit_price:,}", text_size=12, height=28, width=70, text_align=ft.TextAlign.RIGHT, border=ft.border.all(1, ft.Colors.BLUE_200), bgcolor=ft.Colors.WHITE, content_padding=ft.padding.all(5), on_change=lambda e, idx=i: on_update_field(idx, "unit_price", e.control.value.replace(",", "")), keyboard_type=ft.KeyboardType.NUMBER, ) delete_button = ft.IconButton( ft.Icons.DELETE_OUTLINE, tooltip="行を削除", icon_color=ft.Colors.RED_600, disabled=is_locked, icon_size=16, on_click=lambda _, idx=i: on_delete_row(idx), ) subtotal_text = ft.Text( f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT, ) if row_refs is not None: row_refs[i] = { "product": product_field, "quantity": quantity_field, "unit_price": unit_price_field, "subtotal": subtotal_text, } data_rows.append( ft.Row( [ product_field, quantity_field, unit_price_field, subtotal_text, delete_button, ], key=f"row-{i}-{item.description}", ) ) list_control: ft.Control = ft.Column(data_rows, spacing=4) return ft.Column( [ header_row, ft.Divider(height=1, color=ft.Colors.GREY_400), ft.Container( content=list_control, height=250, padding=0, bgcolor=None, border_radius=0, expand=False, ), ] )