"""編集系画面で再利用するEditorフレームワーク(最小版)。""" from dataclasses import dataclass from typing import Callable, List import flet as ft from models.invoice_models import InvoiceItem @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), ] ) ) return ft.Column( [ header_row, ft.Divider(height=1, color=ft.Colors.GREY_400), ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), ] ) 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], ) -> 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 i, item in enumerate(items): product_field = ft.TextField( value=item.description, text_size=12, height=28, expand=True, 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, "description", e.control.value), ) 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), ) data_rows.append( ft.Row( [ product_field, quantity_field, unit_price_field, ft.Text( f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT, ), delete_button, ] ) ) return ft.Column( [ header_row, ft.Divider(height=1, color=ft.Colors.GREY_400), ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), ] )