mini汚染を修復
This commit is contained in:
parent
3e4af336f8
commit
0f86ee14ac
2 changed files with 625 additions and 120 deletions
|
|
@ -117,7 +117,7 @@ def build_invoice_items_edit_table(
|
|||
"""編集モードの明細テーブル。"""
|
||||
header_row = ft.Row(
|
||||
[
|
||||
ft.Container(width=28),
|
||||
ft.Container(width=32),
|
||||
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),
|
||||
|
|
@ -193,21 +193,35 @@ def build_invoice_items_edit_table(
|
|||
"subtotal": subtotal_text,
|
||||
}
|
||||
|
||||
handle = ft.Icon(
|
||||
ft.Icons.DRAG_HANDLE,
|
||||
size=18,
|
||||
color=ft.Colors.BLUE_GREY_400,
|
||||
visible=enable_reorder and not is_locked,
|
||||
if enable_reorder and on_reorder and not is_locked:
|
||||
up_button = ft.IconButton(
|
||||
ft.Icons.ARROW_UPWARD,
|
||||
icon_size=16,
|
||||
tooltip="上へ",
|
||||
disabled=i == 0,
|
||||
on_click=(lambda _, idx=i: on_reorder(idx, idx - 1)) if i > 0 else None,
|
||||
)
|
||||
down_button = ft.IconButton(
|
||||
ft.Icons.ARROW_DOWNWARD,
|
||||
icon_size=16,
|
||||
tooltip="下へ",
|
||||
disabled=i == len(items) - 1,
|
||||
on_click=(lambda _, idx=i: on_reorder(idx, idx + 1)) if i < len(items) - 1 else None,
|
||||
)
|
||||
reorder_buttons = ft.Column([up_button, down_button], spacing=0, width=32)
|
||||
delete_slot = ft.Container(width=0)
|
||||
else:
|
||||
reorder_buttons = ft.Container(width=0)
|
||||
delete_slot = delete_button
|
||||
|
||||
row_control = ft.Row(
|
||||
[
|
||||
handle,
|
||||
reorder_buttons,
|
||||
product_field,
|
||||
quantity_field,
|
||||
unit_price_field,
|
||||
subtotal_text,
|
||||
delete_button,
|
||||
delete_slot,
|
||||
],
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
key=f"row-{i}-{item.description}",
|
||||
|
|
@ -215,17 +229,6 @@ def build_invoice_items_edit_table(
|
|||
|
||||
data_rows.append(row_control)
|
||||
|
||||
if enable_reorder and on_reorder and not is_locked:
|
||||
reorder_items = [
|
||||
ft.ReorderableListViewItem(key=str(idx), content=row)
|
||||
for idx, row in enumerate(data_rows)
|
||||
]
|
||||
list_control = ft.ReorderableListView(
|
||||
controls=reorder_items,
|
||||
on_reorder=lambda e: on_reorder(e.old_index, e.new_index),
|
||||
shrink_wrap=True,
|
||||
)
|
||||
else:
|
||||
list_control = ft.Column(data_rows, spacing=4)
|
||||
|
||||
return ft.Column(
|
||||
|
|
|
|||
692
main.py
692
main.py
|
|
@ -10,8 +10,8 @@ import logging
|
|||
import asyncio
|
||||
import threading
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, date, time
|
||||
from typing import List, Dict, Optional, Any
|
||||
from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer, InvoiceItem, Product
|
||||
from components.customer_picker import CustomerPickerModal
|
||||
from components.explorer_framework import (
|
||||
|
|
@ -75,7 +75,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, trailing_controls: Optional[list] = None,
|
||||
title_control: Optional[ft.Control] = None):
|
||||
title_control: Optional[ft.Control] = None, leading_control: Optional[ft.Control] = None):
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.show_back = show_back
|
||||
|
|
@ -88,6 +88,7 @@ class AppBar(ft.Container):
|
|||
self.bottom = bottom
|
||||
self.trailing_controls = trailing_controls or []
|
||||
self.title_control = title_control
|
||||
self.leading_control = leading_control
|
||||
|
||||
self.bgcolor = ft.Colors.BLUE_GREY_50
|
||||
self.padding = ft.Padding.symmetric(horizontal=16, vertical=8)
|
||||
|
|
@ -108,6 +109,8 @@ class AppBar(ft.Container):
|
|||
on_click=self.on_back if self.on_back else None
|
||||
)
|
||||
)
|
||||
elif self.leading_control is not None:
|
||||
controls.append(self.leading_control)
|
||||
else:
|
||||
controls.append(ft.Container(width=48)) # スペーーサー
|
||||
|
||||
|
|
@ -213,10 +216,41 @@ class FlutterStyleDashboard:
|
|||
self.app_service = AppService()
|
||||
self.invoices = []
|
||||
self.customers = []
|
||||
self.settings_state = {
|
||||
"theme": self.current_theme,
|
||||
"stay_on_detail_after_save": self.stay_on_detail_after_save,
|
||||
"company_name": "",
|
||||
"company_kana": "",
|
||||
"company_address": "",
|
||||
"company_phone": "",
|
||||
"company_representative": "",
|
||||
"company_email": "",
|
||||
"smtp_host": "",
|
||||
"smtp_port": "",
|
||||
"smtp_username": "",
|
||||
"smtp_password": "",
|
||||
"backup_path": "",
|
||||
"corner_stamp_path": "",
|
||||
"rep_stamp_path": "",
|
||||
"bank_accounts": [{"active": False} for _ in range(4)],
|
||||
}
|
||||
self._item_row_refs: Dict[int, Dict[str, ft.Control]] = {}
|
||||
self._total_amount_text: Optional[ft.Text] = None
|
||||
self._tax_amount_text: Optional[ft.Text] = None
|
||||
self._subtotal_text: Optional[ft.Text] = None
|
||||
self.is_reorder_mode = False
|
||||
self.is_company_settings_open = False
|
||||
self._pending_stamp_target: Optional[str] = None
|
||||
self._stamp_picker: Optional[ft.FilePicker] = None
|
||||
self.max_active_bank_accounts = 2
|
||||
self.edit_button_style = ft.ButtonStyle(
|
||||
bgcolor=ft.Colors.WHITE,
|
||||
color=ft.Colors.BLUE_GREY_800,
|
||||
padding=ft.Padding.symmetric(horizontal=12, vertical=8),
|
||||
shape=ft.RoundedRectangleBorder(radius=6),
|
||||
side=ft.BorderSide(1, ft.Colors.BLUE_200),
|
||||
overlay_color=ft.Colors.BLUE_50,
|
||||
)
|
||||
|
||||
self.setup_page()
|
||||
self.setup_database()
|
||||
|
|
@ -300,23 +334,400 @@ class FlutterStyleDashboard:
|
|||
self.page.add(
|
||||
ft.Column([
|
||||
self.main_content,
|
||||
], expand=True)
|
||||
], expand=True),
|
||||
)
|
||||
|
||||
self._settings_hide_task = None
|
||||
|
||||
self.settings_panel_wrapper = ft.Container(
|
||||
width=640,
|
||||
bgcolor=ft.Colors.WHITE,
|
||||
padding=ft.Padding.symmetric(horizontal=16, vertical=24),
|
||||
shadow=[ft.BoxShadow(blur_radius=24, color=ft.Colors.BLACK12, offset=ft.Offset(6, 0))],
|
||||
)
|
||||
self.settings_drawer_overlay = ft.Stack(
|
||||
controls=[
|
||||
ft.Container(
|
||||
expand=True,
|
||||
bgcolor=ft.Colors.BLACK54,
|
||||
on_click=self.close_settings_drawer,
|
||||
),
|
||||
ft.Container(
|
||||
expand=True,
|
||||
alignment=ft.Alignment(-1, 0),
|
||||
content=ft.Container(
|
||||
content=self.settings_panel_wrapper,
|
||||
width=640,
|
||||
bgcolor=ft.Colors.WHITE,
|
||||
),
|
||||
),
|
||||
],
|
||||
expand=True,
|
||||
visible=False,
|
||||
)
|
||||
self.page.overlay.append(self.settings_drawer_overlay)
|
||||
|
||||
# 初期表示
|
||||
self.update_main_content()
|
||||
|
||||
def dispose(self, e=None):
|
||||
"""リソース解放"""
|
||||
def dispose(self, _=None):
|
||||
logging.info("FlutterStyleDashboard.dispose")
|
||||
try:
|
||||
self.app_service.close()
|
||||
except Exception as err:
|
||||
logging.warning(f"クリーンアップ失敗: {err}")
|
||||
if self.page:
|
||||
self.page.window.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"dispose error: {e}")
|
||||
|
||||
def open_settings_drawer(self, _=None):
|
||||
if not hasattr(self, "settings_drawer_overlay"):
|
||||
return
|
||||
if self._settings_hide_task:
|
||||
self._settings_hide_task.cancel()
|
||||
self._settings_hide_task = None
|
||||
self.settings_panel_wrapper.content = self._build_settings_panel()
|
||||
self.settings_drawer_overlay.visible = True
|
||||
self.page.update()
|
||||
|
||||
def close_settings_drawer(self, _=None):
|
||||
if not hasattr(self, "settings_drawer_overlay"):
|
||||
return
|
||||
self.page.update()
|
||||
loop = asyncio.get_event_loop()
|
||||
if self._settings_hide_task:
|
||||
self._settings_hide_task.cancel()
|
||||
self._settings_hide_task = loop.create_task(self._hide_settings_drawer_async())
|
||||
|
||||
async def _hide_settings_drawer_async(self):
|
||||
await asyncio.sleep(0.3)
|
||||
if hasattr(self, "settings_drawer_overlay"):
|
||||
self.settings_drawer_overlay.visible = False
|
||||
self.page.update()
|
||||
self._settings_hide_task = None
|
||||
|
||||
def _build_settings_panel(self) -> ft.Row:
|
||||
sidebar_width = int(self.settings_panel_wrapper.width * 0.25)
|
||||
sidebar = ft.Container(
|
||||
width=sidebar_width,
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Text("メニュー", weight=ft.FontWeight.BOLD),
|
||||
ft.Divider(),
|
||||
ft.ListTile(
|
||||
leading=ft.Icon(ft.Icons.PALETTE),
|
||||
title=ft.Text("全体設定"),
|
||||
selected=not self.is_company_settings_open,
|
||||
on_click=lambda _: self._open_settings_page(False),
|
||||
),
|
||||
ft.ListTile(
|
||||
leading=ft.Icon(ft.Icons.BUSINESS),
|
||||
title=ft.Text("自社情報"),
|
||||
selected=self.is_company_settings_open,
|
||||
on_click=lambda _: self._open_settings_page(True),
|
||||
),
|
||||
],
|
||||
spacing=8,
|
||||
expand=True,
|
||||
),
|
||||
)
|
||||
|
||||
content = self._build_company_settings_content() if self.is_company_settings_open else self._build_settings_content()
|
||||
|
||||
body = ft.Row(
|
||||
[
|
||||
sidebar,
|
||||
ft.VerticalDivider(width=1),
|
||||
ft.Container(content=content, expand=True),
|
||||
],
|
||||
expand=True,
|
||||
)
|
||||
|
||||
header = ft.Row(
|
||||
[
|
||||
ft.IconButton(ft.Icons.ARROW_BACK, tooltip="閉じる", on_click=self.close_settings_drawer),
|
||||
ft.Text("設定", size=18, weight=ft.FontWeight.BOLD),
|
||||
],
|
||||
spacing=8,
|
||||
alignment=ft.MainAxisAlignment.START,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
)
|
||||
|
||||
return ft.Column(
|
||||
[
|
||||
header,
|
||||
ft.Divider(),
|
||||
body,
|
||||
],
|
||||
spacing=8,
|
||||
expand=True,
|
||||
)
|
||||
|
||||
def _open_settings_page(self, company: bool):
|
||||
self.is_company_settings_open = company
|
||||
self.settings_panel_wrapper.content = self._build_settings_panel()
|
||||
self.page.update()
|
||||
|
||||
def _build_settings_content(self) -> ft.Column:
|
||||
theme_radio = ft.RadioGroup(
|
||||
value=self.settings_state.get("theme", self.current_theme),
|
||||
on_change=self._on_theme_change,
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Radio(value=name, label=name.title())
|
||||
for name in self.theme_presets.keys()
|
||||
],
|
||||
spacing=4,
|
||||
),
|
||||
)
|
||||
|
||||
stay_switch = ft.Switch(
|
||||
label="保存後も編集画面に留まる",
|
||||
value=self.settings_state.get("stay_on_detail_after_save", self.stay_on_detail_after_save),
|
||||
on_change=self._on_stay_setting_change,
|
||||
)
|
||||
|
||||
def text_field(key: str, label: str, password: bool = False) -> ft.TextField:
|
||||
return ft.TextField(
|
||||
label=label,
|
||||
value=self.settings_state.get(key, ""),
|
||||
password=password,
|
||||
can_reveal_password=password,
|
||||
on_change=lambda e, k=key: self._on_settings_text_change(k, e.control.value),
|
||||
)
|
||||
|
||||
smtp_section = ft.Column(
|
||||
[
|
||||
ft.Text("SMTPサーバ設定", weight=ft.FontWeight.BOLD),
|
||||
text_field("smtp_host", "ホスト"),
|
||||
text_field("smtp_port", "ポート"),
|
||||
text_field("smtp_username", "ユーザー名"),
|
||||
text_field("smtp_password", "パスワード", password=True),
|
||||
],
|
||||
spacing=8,
|
||||
)
|
||||
|
||||
backup_section = ft.Column(
|
||||
[
|
||||
ft.Text("バックアップ先", weight=ft.FontWeight.BOLD),
|
||||
text_field("backup_path", "保存フォルダ"),
|
||||
],
|
||||
spacing=8,
|
||||
)
|
||||
|
||||
action_buttons = ft.Row(
|
||||
[
|
||||
ft.ElevatedButton("保存", icon=ft.Icons.SAVE, on_click=self._save_settings_and_close),
|
||||
ft.TextButton("閉じる", on_click=self.close_settings_drawer),
|
||||
],
|
||||
spacing=12,
|
||||
alignment=ft.MainAxisAlignment.END,
|
||||
)
|
||||
|
||||
return ft.Column(
|
||||
[
|
||||
ft.Text("設定", size=20, weight=ft.FontWeight.BOLD),
|
||||
ft.Divider(),
|
||||
ft.Text("テーマ", weight=ft.FontWeight.BOLD),
|
||||
theme_radio,
|
||||
stay_switch,
|
||||
ft.Divider(),
|
||||
smtp_section,
|
||||
ft.Divider(),
|
||||
backup_section,
|
||||
ft.Divider(),
|
||||
action_buttons,
|
||||
],
|
||||
spacing=16,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
|
||||
def _build_company_settings_content(self) -> ft.Column:
|
||||
def text_field(key: str, label: str, multiline: bool = False) -> ft.TextField:
|
||||
return ft.TextField(
|
||||
label=label,
|
||||
value=self.settings_state.get(key, ""),
|
||||
multiline=multiline,
|
||||
min_lines=1 if not multiline else 2,
|
||||
max_lines=4,
|
||||
on_change=lambda e, k=key: self._on_settings_text_change(k, e.control.value),
|
||||
)
|
||||
|
||||
def bank_entry(index: int, data: Dict[str, Any]) -> ft.Container:
|
||||
prefix = f"bank_{index}"
|
||||
|
||||
def on_field_change(e, field):
|
||||
account = self._ensure_bank_account(index)
|
||||
account[field] = e.control.value
|
||||
self._save_bank_accounts()
|
||||
|
||||
def on_active_change(e):
|
||||
account = self._ensure_bank_account(index)
|
||||
is_enabling = bool(e.control.value)
|
||||
if is_enabling and self._active_bank_count() >= self.max_active_bank_accounts:
|
||||
self._show_snack(f"請求書掲載は最大{self.max_active_bank_accounts}口座です", ft.Colors.RED_200)
|
||||
e.control.value = False
|
||||
self.page.update()
|
||||
return
|
||||
account["active"] = is_enabling
|
||||
self._save_bank_accounts()
|
||||
|
||||
is_active = data.get("active", False)
|
||||
return ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Row(
|
||||
[
|
||||
ft.Text(f"口座 {index + 1}", weight=ft.FontWeight.BOLD),
|
||||
ft.Switch(label="請求書に掲載", value=is_active, on_change=on_active_change),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||
),
|
||||
ft.TextField(label="銀行名", value=data.get("bank_name", ""), on_change=lambda e: on_field_change(e, "bank_name")),
|
||||
ft.TextField(label="支店名", value=data.get("branch_name", ""), on_change=lambda e: on_field_change(e, "branch_name")),
|
||||
ft.TextField(label="区分", value=data.get("account_type", "普通"), on_change=lambda e: on_field_change(e, "account_type")),
|
||||
ft.TextField(label="口座番号", value=data.get("account_number", ""), on_change=lambda e: on_field_change(e, "account_number")),
|
||||
ft.TextField(label="名義", value=data.get("holder", ""), on_change=lambda e: on_field_change(e, "holder")),
|
||||
],
|
||||
spacing=8,
|
||||
),
|
||||
border=ft.Border.all(1, ft.Colors.GREY_300),
|
||||
border_radius=8,
|
||||
padding=ft.Padding.all(12),
|
||||
)
|
||||
|
||||
stamp_controls = ft.Column(
|
||||
[
|
||||
ft.Text("印鑑アップロード", weight=ft.FontWeight.BOLD),
|
||||
ft.Row([
|
||||
ft.Column([
|
||||
ft.Text("角印"),
|
||||
ft.Row([
|
||||
ft.ElevatedButton("アップロード", on_click=lambda _: self._pick_stamp("corner_stamp_path")),
|
||||
ft.Text(self._file_name(self.settings_state.get("corner_stamp_path"))),
|
||||
]),
|
||||
]),
|
||||
ft.Column([
|
||||
ft.Text("担当者印"),
|
||||
ft.Row([
|
||||
ft.ElevatedButton("アップロード", on_click=lambda _: self._pick_stamp("rep_stamp_path")),
|
||||
ft.Text(self._file_name(self.settings_state.get("rep_stamp_path"))),
|
||||
]),
|
||||
]),
|
||||
], spacing=16),
|
||||
],
|
||||
spacing=8,
|
||||
)
|
||||
|
||||
accounts = self.settings_state.get("bank_accounts", [])
|
||||
while len(accounts) < 4:
|
||||
accounts.append({"active": False})
|
||||
account_cards = [bank_entry(i, accounts[i]) for i in range(4)]
|
||||
|
||||
company_fields = ft.Column(
|
||||
[
|
||||
text_field("company_name", "会社名"),
|
||||
text_field("company_kana", "会社名 (カナ)"),
|
||||
text_field("company_address", "住所", multiline=True),
|
||||
text_field("company_phone", "電話番号"),
|
||||
ft.TextField(
|
||||
label="代表者名",
|
||||
value=self.settings_state.get("company_representative", ""),
|
||||
on_change=lambda e: self._on_settings_text_change("company_representative", e.control.value),
|
||||
),
|
||||
ft.TextField(
|
||||
label="連絡先メール",
|
||||
value=self.settings_state.get("company_email", ""),
|
||||
on_change=lambda e: self._on_settings_text_change("company_email", e.control.value),
|
||||
),
|
||||
],
|
||||
spacing=10,
|
||||
)
|
||||
|
||||
return ft.Column(
|
||||
[
|
||||
ft.Text("自社情報設定", size=18, weight=ft.FontWeight.BOLD),
|
||||
ft.Divider(),
|
||||
company_fields,
|
||||
ft.Divider(),
|
||||
ft.Text("銀行口座", weight=ft.FontWeight.BOLD),
|
||||
ft.Text("最大4口座登録可能・2口座まで請求書に掲載できます"),
|
||||
ft.Column(account_cards, spacing=12),
|
||||
ft.Divider(),
|
||||
stamp_controls,
|
||||
ft.Divider(),
|
||||
ft.Row([
|
||||
ft.ElevatedButton("保存", icon=ft.Icons.SAVE, on_click=self._save_settings_and_close),
|
||||
ft.TextButton("閉じる", on_click=self.close_settings_drawer),
|
||||
], alignment=ft.MainAxisAlignment.END, spacing=12),
|
||||
],
|
||||
spacing=16,
|
||||
expand=True,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
)
|
||||
|
||||
def _ensure_bank_account(self, index: int) -> Dict[str, Any]:
|
||||
accounts = self.settings_state.setdefault("bank_accounts", [])
|
||||
while len(accounts) <= index:
|
||||
accounts.append({"active": False})
|
||||
return accounts[index]
|
||||
|
||||
def _save_bank_accounts(self):
|
||||
self.page.update()
|
||||
|
||||
def _active_bank_count(self) -> int:
|
||||
accounts = self.settings_state.get("bank_accounts", [])
|
||||
return sum(1 for acc in accounts if acc.get("active"))
|
||||
|
||||
def _file_name(self, path: str) -> str:
|
||||
if not path:
|
||||
return "未設定"
|
||||
return path.split("/")[-1]
|
||||
|
||||
def _pick_stamp(self, target_key: str):
|
||||
if self._stamp_picker is None:
|
||||
self._show_snack("この環境では印鑑アップロードに対応していません", ft.Colors.RED_200)
|
||||
return
|
||||
self._pending_stamp_target = target_key
|
||||
self._stamp_picker.pick_files(allow_multiple=False)
|
||||
|
||||
def _on_stamp_file_picked(self, e):
|
||||
if not e.files or not self._pending_stamp_target:
|
||||
return
|
||||
file = e.files[0]
|
||||
temp_path = file.path or file.name
|
||||
self.settings_state[self._pending_stamp_target] = temp_path
|
||||
self._pending_stamp_target = None
|
||||
self.settings_panel_wrapper.content = self._build_settings_panel()
|
||||
self.page.update()
|
||||
|
||||
def _on_theme_change(self, e: ft.ControlEvent):
|
||||
new_theme = e.control.value or "light"
|
||||
if new_theme == self.current_theme:
|
||||
return
|
||||
self.settings_state["theme"] = new_theme
|
||||
self.apply_theme(new_theme)
|
||||
self.update_main_content()
|
||||
|
||||
def _on_stay_setting_change(self, e: ft.ControlEvent):
|
||||
value = bool(e.control.value)
|
||||
self.settings_state["stay_on_detail_after_save"] = value
|
||||
self.stay_on_detail_after_save = value
|
||||
|
||||
def _on_settings_text_change(self, key: str, value: str):
|
||||
self.settings_state[key] = value
|
||||
|
||||
def _save_settings_and_close(self, _=None):
|
||||
self._show_snack("設定を保存しました", ft.Colors.BLUE_GREY_600)
|
||||
self.close_settings_drawer()
|
||||
|
||||
def toggle_reorder_mode(self, _=None):
|
||||
self.is_reorder_mode = not self.is_reorder_mode
|
||||
self.update_main_content()
|
||||
|
||||
def on_tab_change(self, index):
|
||||
"""タブ切り替え"""
|
||||
self.current_tab = index
|
||||
self.update_main_content()
|
||||
|
||||
self.page.update()
|
||||
|
||||
@log_wrap("update_main_content")
|
||||
|
|
@ -366,21 +777,20 @@ class FlutterStyleDashboard:
|
|||
logging.info("_build_invoice_list_screen: 開始")
|
||||
|
||||
# AppBar(戻るボタンなし、編集ボタンなし)
|
||||
settings_button = ft.IconButton(
|
||||
icon=ft.Icons.MENU,
|
||||
tooltip="設定",
|
||||
on_click=self.open_settings_drawer,
|
||||
)
|
||||
|
||||
app_bar = AppBar(
|
||||
title="伝票一覧",
|
||||
show_back=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)),
|
||||
)
|
||||
ft.IconButton(ft.Icons.REFRESH, on_click=lambda _: self.refresh_invoices()),
|
||||
],
|
||||
leading_control=settings_button,
|
||||
)
|
||||
logging.info("_build_invoice_list_screen: AppBar作成完了")
|
||||
|
||||
|
|
@ -1256,6 +1666,7 @@ class FlutterStyleDashboard:
|
|||
def set_doc_type(dt: DocumentType):
|
||||
if self.is_detail_edit_mode and not is_locked:
|
||||
self.select_document_type(dt)
|
||||
self.update_main_content()
|
||||
|
||||
doc_type_items = [
|
||||
ft.PopupMenuItem(
|
||||
|
|
@ -1270,19 +1681,51 @@ class FlutterStyleDashboard:
|
|||
self.editing_invoice.is_draft = bool(e.control.value)
|
||||
self.update_main_content()
|
||||
|
||||
if self.is_detail_edit_mode and not is_locked:
|
||||
doc_type_menu = ft.PopupMenuButton(
|
||||
content=ft.Text(
|
||||
doc_type_label = ft.Row(
|
||||
[
|
||||
ft.Icon(ft.Icons.DESCRIPTION, size=14, color=ft.Colors.BLUE_GREY_700),
|
||||
ft.Text(
|
||||
self.selected_document_type.value,
|
||||
size=12,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
color=ft.Colors.WHITE,
|
||||
color=ft.Colors.BLUE_GREY_800,
|
||||
),
|
||||
],
|
||||
spacing=4,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
)
|
||||
doc_type_chip = ft.Container(
|
||||
content=doc_type_label,
|
||||
padding=ft.Padding.symmetric(horizontal=12, vertical=6),
|
||||
bgcolor=ft.Colors.WHITE,
|
||||
border=ft.border.all(1, ft.Colors.BLUE_200),
|
||||
border_radius=8,
|
||||
)
|
||||
|
||||
if self.is_detail_edit_mode and not is_locked:
|
||||
doc_type_control = ft.PopupMenuButton(
|
||||
content=doc_type_chip,
|
||||
items=doc_type_items,
|
||||
tooltip="帳票タイプ変更",
|
||||
)
|
||||
else:
|
||||
doc_type_menu = None
|
||||
doc_type_control = doc_type_chip
|
||||
|
||||
if self.is_detail_edit_mode and not is_locked:
|
||||
draft_control = ft.Switch(
|
||||
label="下書き",
|
||||
value=is_draft,
|
||||
on_change=toggle_draft,
|
||||
)
|
||||
elif is_draft:
|
||||
draft_control = ft.Container(
|
||||
content=ft.Text("下書き", size=11, color=ft.Colors.BROWN_800),
|
||||
padding=ft.Padding.symmetric(horizontal=8, vertical=4),
|
||||
bgcolor=ft.Colors.BROWN_100,
|
||||
border_radius=12,
|
||||
)
|
||||
else:
|
||||
draft_control = ft.Container()
|
||||
|
||||
# 日付・時間ピッカー(テキスト入力NGなのでボタン+ポップアップ)
|
||||
date_button = None
|
||||
|
|
@ -1299,18 +1742,21 @@ class FlutterStyleDashboard:
|
|||
self._time_picker = ft.TimePicker()
|
||||
self.page.overlay.append(self._time_picker)
|
||||
|
||||
def _parse_picker_date(value) -> date:
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if hasattr(value, "year") and hasattr(value, "month") and hasattr(value, "day"):
|
||||
return date(value.year, value.month, value.day)
|
||||
raw = str(value)
|
||||
if "T" in raw:
|
||||
raw = raw.split("T")[0]
|
||||
return date.fromisoformat(raw)
|
||||
|
||||
def on_date_change(e):
|
||||
if not e.data:
|
||||
return
|
||||
try:
|
||||
if isinstance(e.data, datetime):
|
||||
picked_date = e.data
|
||||
elif hasattr(e.data, "year") and hasattr(e.data, "month") and hasattr(e.data, "day"):
|
||||
picked_date = datetime(e.data.year, e.data.month, e.data.day)
|
||||
else:
|
||||
raw = str(e.data)
|
||||
picked_date = datetime.fromisoformat(raw)
|
||||
|
||||
picked_date = _parse_picker_date(e.data)
|
||||
current = self.editing_invoice.date
|
||||
self.editing_invoice.date = datetime(
|
||||
picked_date.year, picked_date.month, picked_date.day,
|
||||
|
|
@ -1361,6 +1807,7 @@ class FlutterStyleDashboard:
|
|||
ft.Text(self.editing_invoice.date.strftime("%Y/%m/%d"), size=12),
|
||||
], spacing=6),
|
||||
on_click=lambda _: self._open_date_picker(),
|
||||
style=self.edit_button_style,
|
||||
)
|
||||
time_button = ft.Button(
|
||||
content=ft.Row([
|
||||
|
|
@ -1368,6 +1815,7 @@ class FlutterStyleDashboard:
|
|||
ft.Text(self.editing_invoice.date.strftime("%H:%M"), size=12),
|
||||
], spacing=6),
|
||||
on_click=lambda _: self._open_time_picker(),
|
||||
style=self.edit_button_style,
|
||||
)
|
||||
|
||||
# 備考フィールド
|
||||
|
|
@ -1567,15 +2015,11 @@ class FlutterStyleDashboard:
|
|||
|
||||
if (not is_view_mode and not is_locked) or is_new_invoice:
|
||||
customer_control = ft.Button(
|
||||
content=ft.Text(customer_label, no_wrap=True),
|
||||
content=ft.Text(customer_label, no_wrap=True, color=ft.Colors.BLUE_GREY_800),
|
||||
width=220,
|
||||
height=36,
|
||||
on_click=lambda _: select_customer(),
|
||||
style=ft.ButtonStyle(
|
||||
padding=ft.Padding.symmetric(horizontal=10, vertical=6),
|
||||
bgcolor=ft.Colors.BLUE_GREY_100,
|
||||
shape=ft.RoundedRectangleBorder(radius=6),
|
||||
),
|
||||
style=self.edit_button_style,
|
||||
)
|
||||
else:
|
||||
customer_control = ft.Text(
|
||||
|
|
@ -1584,42 +2028,6 @@ class FlutterStyleDashboard:
|
|||
weight=ft.FontWeight.BOLD,
|
||||
)
|
||||
|
||||
if self.is_detail_edit_mode and not is_locked:
|
||||
doc_type_control = ft.PopupMenuButton(
|
||||
content=ft.Text(
|
||||
self.selected_document_type.value,
|
||||
size=12,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
color=ft.Colors.WHITE,
|
||||
),
|
||||
items=doc_type_items,
|
||||
tooltip="帳票タイプ変更",
|
||||
)
|
||||
else:
|
||||
doc_type_control = ft.Text(
|
||||
self.selected_document_type.value,
|
||||
size=12,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
color=ft.Colors.WHITE,
|
||||
)
|
||||
|
||||
draft_control: ft.Control
|
||||
if self.is_detail_edit_mode and not is_locked:
|
||||
draft_control = ft.Switch(
|
||||
label="下書き",
|
||||
value=is_draft,
|
||||
on_change=toggle_draft,
|
||||
)
|
||||
elif is_draft:
|
||||
draft_control = ft.Container(
|
||||
content=ft.Text("下書き", size=11, color=ft.Colors.BROWN_800),
|
||||
padding=ft.Padding.symmetric(horizontal=8, vertical=4),
|
||||
bgcolor=ft.Colors.BROWN_100,
|
||||
border_radius=12,
|
||||
)
|
||||
else:
|
||||
draft_control = ft.Container()
|
||||
|
||||
date_time_row = ft.Row(
|
||||
[
|
||||
date_button if date_button else ft.Text(
|
||||
|
|
@ -1633,7 +2041,7 @@ class FlutterStyleDashboard:
|
|||
color=ft.Colors.BLUE_GREY_600,
|
||||
),
|
||||
],
|
||||
spacing=8,
|
||||
spacing=12,
|
||||
)
|
||||
|
||||
customer_block = ft.Column(
|
||||
|
|
@ -1652,17 +2060,7 @@ class FlutterStyleDashboard:
|
|||
[
|
||||
ft.Row(
|
||||
[
|
||||
ft.Container(
|
||||
content=doc_type_control if self.is_detail_edit_mode and not is_locked else ft.Text(
|
||||
self.selected_document_type.value,
|
||||
size=9,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
color=self.invoice_card_theme["tag_text_color"],
|
||||
),
|
||||
padding=ft.Padding.symmetric(horizontal=8, vertical=2),
|
||||
bgcolor=self.invoice_card_theme["tag_bg"],
|
||||
border_radius=10,
|
||||
),
|
||||
doc_type_control,
|
||||
ft.Text(
|
||||
f"No: {self.editing_invoice.invoice_number}",
|
||||
size=10,
|
||||
|
|
@ -1701,14 +2099,26 @@ class FlutterStyleDashboard:
|
|||
[
|
||||
ft.Text("明細", size=13, weight=ft.FontWeight.BOLD),
|
||||
ft.Container(expand=True),
|
||||
ft.IconButton(
|
||||
ft.Icons.ADD_CIRCLE_OUTLINE,
|
||||
tooltip="行を追加",
|
||||
icon_color=ft.Colors.GREEN_600,
|
||||
ft.Button(
|
||||
content=ft.Row([
|
||||
ft.Icon(ft.Icons.SHUFFLE, size=16, color=ft.Colors.BLUE_GREY_700),
|
||||
ft.Text("並べ替え" + ("ON" if self.is_reorder_mode else ""), size=12),
|
||||
], spacing=6),
|
||||
style=self.edit_button_style,
|
||||
on_click=self.toggle_reorder_mode,
|
||||
disabled=is_locked or is_view_mode,
|
||||
) if not is_locked and not is_view_mode else ft.Container(),
|
||||
ft.Button(
|
||||
content=ft.Row([
|
||||
ft.Icon(ft.Icons.ADD_CIRCLE_OUTLINE, size=16, color=ft.Colors.BLUE_GREY_700),
|
||||
ft.Text("行追加", size=12),
|
||||
], spacing=6),
|
||||
style=self.edit_button_style,
|
||||
on_click=lambda _: self._add_item_row(),
|
||||
disabled=is_locked or is_view_mode,
|
||||
) if not is_locked and not is_view_mode else ft.Container(),
|
||||
],
|
||||
spacing=8,
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
),
|
||||
ft.Container(
|
||||
|
|
@ -1734,13 +2144,10 @@ class FlutterStyleDashboard:
|
|||
[
|
||||
ft.Button(
|
||||
content=ft.Row([
|
||||
ft.Icon(ft.Icons.DOWNLOAD, size=16),
|
||||
ft.Icon(ft.Icons.DOWNLOAD, size=16, color=ft.Colors.BLUE_GREY_700),
|
||||
ft.Text("PDF生成", size=12),
|
||||
], spacing=6),
|
||||
style=ft.ButtonStyle(
|
||||
bgcolor=ft.Colors.BLUE_600,
|
||||
color=ft.Colors.WHITE,
|
||||
),
|
||||
style=self.edit_button_style,
|
||||
on_click=lambda _: self.generate_pdf_from_edit(),
|
||||
disabled=is_locked,
|
||||
) if not is_locked else ft.Container(),
|
||||
|
|
@ -1942,7 +2349,7 @@ class FlutterStyleDashboard:
|
|||
products=self.app_service.product.get_all_products(),
|
||||
on_product_select=self._open_product_picker_for_row,
|
||||
row_refs=self._ensure_item_row_refs(),
|
||||
enable_reorder=not is_locked,
|
||||
enable_reorder=not is_locked and self.is_reorder_mode,
|
||||
on_reorder=lambda old, new: self._reorder_item_row(old, new),
|
||||
)
|
||||
|
||||
|
|
@ -2154,6 +2561,101 @@ class FlutterStyleDashboard:
|
|||
self.page.update()
|
||||
except Exception as e:
|
||||
logging.warning(f"toast hide failed: {e}")
|
||||
|
||||
def _build_theme_presets(self) -> Dict[str, Dict[str, Any]]:
|
||||
common_radius = 18
|
||||
light_palette = {
|
||||
"page_bg": "#F3F2FB",
|
||||
"card_bg": ft.Colors.WHITE,
|
||||
"card_radius": common_radius,
|
||||
"shadow": ft.BoxShadow(
|
||||
blur_radius=16,
|
||||
spread_radius=0,
|
||||
color="#D5D8F0",
|
||||
offset=ft.Offset(0, 6),
|
||||
),
|
||||
"icon_default_bg": "#5C6BC0",
|
||||
"title_color": ft.Colors.BLUE_GREY_900,
|
||||
"subtitle_color": ft.Colors.BLUE_GREY_500,
|
||||
"amount_color": "#2F3C7E",
|
||||
"tag_text_color": "#4B4F67",
|
||||
"tag_bg": "#E7E9FB",
|
||||
"draft_card_bg": "#F5EEE4",
|
||||
"draft_border": "#D7C4AF",
|
||||
"draft_shadow_highlight": ft.BoxShadow(
|
||||
blur_radius=8,
|
||||
spread_radius=0,
|
||||
color="#FFFFFF",
|
||||
offset=ft.Offset(-2, -2),
|
||||
),
|
||||
"draft_shadow_depth": ft.BoxShadow(
|
||||
blur_radius=14,
|
||||
spread_radius=2,
|
||||
color="#C3A88C",
|
||||
offset=ft.Offset(4, 6),
|
||||
),
|
||||
"badge_bg": "#35C46B",
|
||||
"doc_type_palette": {
|
||||
DocumentType.INVOICE.value: "#5C6BC0",
|
||||
DocumentType.ESTIMATE.value: "#7E57C2",
|
||||
DocumentType.DELIVERY.value: "#26A69A",
|
||||
DocumentType.RECEIPT.value: "#FF7043",
|
||||
DocumentType.SALES.value: "#42A5F5",
|
||||
DocumentType.DRAFT.value: "#90A4AE",
|
||||
},
|
||||
}
|
||||
|
||||
monokai_palette = {
|
||||
"page_bg": "#272822",
|
||||
"card_bg": "#3E3D32",
|
||||
"card_radius": common_radius,
|
||||
"shadow": ft.BoxShadow(
|
||||
blur_radius=12,
|
||||
spread_radius=0,
|
||||
color="#00000055",
|
||||
offset=ft.Offset(0, 4),
|
||||
),
|
||||
"icon_default_bg": "#F92672",
|
||||
"title_color": "#F8F8F2",
|
||||
"subtitle_color": "#A6E22E",
|
||||
"amount_color": "#66D9EF",
|
||||
"tag_text_color": "#F8F8F2",
|
||||
"tag_bg": "#75715E",
|
||||
"draft_card_bg": "#4F3F2F",
|
||||
"draft_border": "#CDAA7D",
|
||||
"draft_shadow_highlight": ft.BoxShadow(
|
||||
blur_radius=6,
|
||||
spread_radius=0,
|
||||
color="#ffffff22",
|
||||
offset=ft.Offset(-1, -1),
|
||||
),
|
||||
"draft_shadow_depth": ft.BoxShadow(
|
||||
blur_radius=10,
|
||||
spread_radius=0,
|
||||
color="#00000055",
|
||||
offset=ft.Offset(2, 3),
|
||||
),
|
||||
"badge_bg": "#AE81FF",
|
||||
"doc_type_palette": {
|
||||
DocumentType.INVOICE.value: "#F92672",
|
||||
DocumentType.ESTIMATE.value: "#AE81FF",
|
||||
DocumentType.DELIVERY.value: "#A6E22E",
|
||||
DocumentType.RECEIPT.value: "#FD971F",
|
||||
DocumentType.SALES.value: "#66D9EF",
|
||||
DocumentType.DRAFT.value: "#75715E",
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"light": light_palette,
|
||||
"monokai": monokai_palette,
|
||||
}
|
||||
|
||||
def apply_theme(self, name: str):
|
||||
preset = self.theme_presets.get(name) or self.theme_presets.get("light")
|
||||
self.current_theme = name if name in self.theme_presets else "light"
|
||||
self.invoice_card_theme = {k: v for k, v in preset.items() if k != "doc_type_palette"}
|
||||
self.doc_type_palette = preset["doc_type_palette"].copy()
|
||||
def create_new_customer_screen(self) -> ft.Container:
|
||||
"""新規/既存顧客登録・編集画面"""
|
||||
editing_customer = getattr(self, "editing_customer_for_form", None)
|
||||
|
|
|
|||
Loading…
Reference in a new issue