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(
|
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=180),
|
||||||
ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35),
|
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),
|
||||||
|
|
@ -193,21 +193,35 @@ def build_invoice_items_edit_table(
|
||||||
"subtotal": subtotal_text,
|
"subtotal": subtotal_text,
|
||||||
}
|
}
|
||||||
|
|
||||||
handle = ft.Icon(
|
if enable_reorder and on_reorder and not is_locked:
|
||||||
ft.Icons.DRAG_HANDLE,
|
up_button = ft.IconButton(
|
||||||
size=18,
|
ft.Icons.ARROW_UPWARD,
|
||||||
color=ft.Colors.BLUE_GREY_400,
|
icon_size=16,
|
||||||
visible=enable_reorder and not is_locked,
|
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(
|
row_control = ft.Row(
|
||||||
[
|
[
|
||||||
handle,
|
reorder_buttons,
|
||||||
product_field,
|
product_field,
|
||||||
quantity_field,
|
quantity_field,
|
||||||
unit_price_field,
|
unit_price_field,
|
||||||
subtotal_text,
|
subtotal_text,
|
||||||
delete_button,
|
delete_slot,
|
||||||
],
|
],
|
||||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
key=f"row-{i}-{item.description}",
|
key=f"row-{i}-{item.description}",
|
||||||
|
|
@ -215,18 +229,7 @@ def build_invoice_items_edit_table(
|
||||||
|
|
||||||
data_rows.append(row_control)
|
data_rows.append(row_control)
|
||||||
|
|
||||||
if enable_reorder and on_reorder and not is_locked:
|
list_control = ft.Column(data_rows, spacing=4)
|
||||||
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(
|
return ft.Column(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
692
main.py
692
main.py
|
|
@ -10,8 +10,8 @@ import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from datetime import datetime
|
from datetime import datetime, date, time
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional, Any
|
||||||
from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer, InvoiceItem, Product
|
from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer, InvoiceItem, Product
|
||||||
from components.customer_picker import CustomerPickerModal
|
from components.customer_picker import CustomerPickerModal
|
||||||
from components.explorer_framework import (
|
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,
|
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 = "編集",
|
on_back=None, on_edit=None, page=None, action_icon=None, action_tooltip: str = "編集",
|
||||||
bottom: Optional[ft.Control] = None, trailing_controls: Optional[list] = None,
|
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__()
|
super().__init__()
|
||||||
self.title = title
|
self.title = title
|
||||||
self.show_back = show_back
|
self.show_back = show_back
|
||||||
|
|
@ -88,6 +88,7 @@ class AppBar(ft.Container):
|
||||||
self.bottom = bottom
|
self.bottom = bottom
|
||||||
self.trailing_controls = trailing_controls or []
|
self.trailing_controls = trailing_controls or []
|
||||||
self.title_control = title_control
|
self.title_control = title_control
|
||||||
|
self.leading_control = leading_control
|
||||||
|
|
||||||
self.bgcolor = ft.Colors.BLUE_GREY_50
|
self.bgcolor = ft.Colors.BLUE_GREY_50
|
||||||
self.padding = ft.Padding.symmetric(horizontal=16, vertical=8)
|
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
|
on_click=self.on_back if self.on_back else None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif self.leading_control is not None:
|
||||||
|
controls.append(self.leading_control)
|
||||||
else:
|
else:
|
||||||
controls.append(ft.Container(width=48)) # スペーーサー
|
controls.append(ft.Container(width=48)) # スペーーサー
|
||||||
|
|
||||||
|
|
@ -213,10 +216,41 @@ class FlutterStyleDashboard:
|
||||||
self.app_service = AppService()
|
self.app_service = AppService()
|
||||||
self.invoices = []
|
self.invoices = []
|
||||||
self.customers = []
|
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._item_row_refs: Dict[int, Dict[str, ft.Control]] = {}
|
||||||
self._total_amount_text: Optional[ft.Text] = None
|
self._total_amount_text: Optional[ft.Text] = None
|
||||||
self._tax_amount_text: Optional[ft.Text] = None
|
self._tax_amount_text: Optional[ft.Text] = None
|
||||||
self._subtotal_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_page()
|
||||||
self.setup_database()
|
self.setup_database()
|
||||||
|
|
@ -300,23 +334,400 @@ class FlutterStyleDashboard:
|
||||||
self.page.add(
|
self.page.add(
|
||||||
ft.Column([
|
ft.Column([
|
||||||
self.main_content,
|
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()
|
self.update_main_content()
|
||||||
|
|
||||||
def dispose(self, e=None):
|
def dispose(self, _=None):
|
||||||
"""リソース解放"""
|
logging.info("FlutterStyleDashboard.dispose")
|
||||||
try:
|
try:
|
||||||
self.app_service.close()
|
if self.page:
|
||||||
except Exception as err:
|
self.page.window.close()
|
||||||
logging.warning(f"クリーンアップ失敗: {err}")
|
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):
|
def on_tab_change(self, index):
|
||||||
"""タブ切り替え"""
|
"""タブ切り替え"""
|
||||||
self.current_tab = index
|
self.current_tab = index
|
||||||
self.update_main_content()
|
self.update_main_content()
|
||||||
|
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
@log_wrap("update_main_content")
|
@log_wrap("update_main_content")
|
||||||
|
|
@ -366,21 +777,20 @@ class FlutterStyleDashboard:
|
||||||
logging.info("_build_invoice_list_screen: 開始")
|
logging.info("_build_invoice_list_screen: 開始")
|
||||||
|
|
||||||
# AppBar(戻るボタンなし、編集ボタンなし)
|
# AppBar(戻るボタンなし、編集ボタンなし)
|
||||||
|
settings_button = ft.IconButton(
|
||||||
|
icon=ft.Icons.MENU,
|
||||||
|
tooltip="設定",
|
||||||
|
on_click=self.open_settings_drawer,
|
||||||
|
)
|
||||||
|
|
||||||
app_bar = AppBar(
|
app_bar = AppBar(
|
||||||
title="伝票一覧",
|
title="伝票一覧",
|
||||||
show_back=False,
|
show_back=False,
|
||||||
show_edit=False,
|
show_edit=False,
|
||||||
trailing_controls=[
|
trailing_controls=[
|
||||||
ft.Button(
|
ft.IconButton(ft.Icons.REFRESH, on_click=lambda _: self.refresh_invoices()),
|
||||||
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)),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
|
leading_control=settings_button,
|
||||||
)
|
)
|
||||||
logging.info("_build_invoice_list_screen: AppBar作成完了")
|
logging.info("_build_invoice_list_screen: AppBar作成完了")
|
||||||
|
|
||||||
|
|
@ -1256,6 +1666,7 @@ class FlutterStyleDashboard:
|
||||||
def set_doc_type(dt: DocumentType):
|
def set_doc_type(dt: DocumentType):
|
||||||
if self.is_detail_edit_mode and not is_locked:
|
if self.is_detail_edit_mode and not is_locked:
|
||||||
self.select_document_type(dt)
|
self.select_document_type(dt)
|
||||||
|
self.update_main_content()
|
||||||
|
|
||||||
doc_type_items = [
|
doc_type_items = [
|
||||||
ft.PopupMenuItem(
|
ft.PopupMenuItem(
|
||||||
|
|
@ -1270,19 +1681,51 @@ class FlutterStyleDashboard:
|
||||||
self.editing_invoice.is_draft = bool(e.control.value)
|
self.editing_invoice.is_draft = bool(e.control.value)
|
||||||
self.update_main_content()
|
self.update_main_content()
|
||||||
|
|
||||||
if self.is_detail_edit_mode and not is_locked:
|
doc_type_label = ft.Row(
|
||||||
doc_type_menu = ft.PopupMenuButton(
|
[
|
||||||
content=ft.Text(
|
ft.Icon(ft.Icons.DESCRIPTION, size=14, color=ft.Colors.BLUE_GREY_700),
|
||||||
|
ft.Text(
|
||||||
self.selected_document_type.value,
|
self.selected_document_type.value,
|
||||||
size=12,
|
size=12,
|
||||||
weight=ft.FontWeight.BOLD,
|
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,
|
items=doc_type_items,
|
||||||
tooltip="帳票タイプ変更",
|
tooltip="帳票タイプ変更",
|
||||||
)
|
)
|
||||||
else:
|
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なのでボタン+ポップアップ)
|
# 日付・時間ピッカー(テキスト入力NGなのでボタン+ポップアップ)
|
||||||
date_button = None
|
date_button = None
|
||||||
|
|
@ -1299,18 +1742,21 @@ class FlutterStyleDashboard:
|
||||||
self._time_picker = ft.TimePicker()
|
self._time_picker = ft.TimePicker()
|
||||||
self.page.overlay.append(self._time_picker)
|
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):
|
def on_date_change(e):
|
||||||
if not e.data:
|
if not e.data:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
if isinstance(e.data, datetime):
|
picked_date = _parse_picker_date(e.data)
|
||||||
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)
|
|
||||||
|
|
||||||
current = self.editing_invoice.date
|
current = self.editing_invoice.date
|
||||||
self.editing_invoice.date = datetime(
|
self.editing_invoice.date = datetime(
|
||||||
picked_date.year, picked_date.month, picked_date.day,
|
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),
|
ft.Text(self.editing_invoice.date.strftime("%Y/%m/%d"), size=12),
|
||||||
], spacing=6),
|
], spacing=6),
|
||||||
on_click=lambda _: self._open_date_picker(),
|
on_click=lambda _: self._open_date_picker(),
|
||||||
|
style=self.edit_button_style,
|
||||||
)
|
)
|
||||||
time_button = ft.Button(
|
time_button = ft.Button(
|
||||||
content=ft.Row([
|
content=ft.Row([
|
||||||
|
|
@ -1368,6 +1815,7 @@ class FlutterStyleDashboard:
|
||||||
ft.Text(self.editing_invoice.date.strftime("%H:%M"), size=12),
|
ft.Text(self.editing_invoice.date.strftime("%H:%M"), size=12),
|
||||||
], spacing=6),
|
], spacing=6),
|
||||||
on_click=lambda _: self._open_time_picker(),
|
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:
|
if (not is_view_mode and not is_locked) or is_new_invoice:
|
||||||
customer_control = ft.Button(
|
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,
|
width=220,
|
||||||
height=36,
|
height=36,
|
||||||
on_click=lambda _: select_customer(),
|
on_click=lambda _: select_customer(),
|
||||||
style=ft.ButtonStyle(
|
style=self.edit_button_style,
|
||||||
padding=ft.Padding.symmetric(horizontal=10, vertical=6),
|
|
||||||
bgcolor=ft.Colors.BLUE_GREY_100,
|
|
||||||
shape=ft.RoundedRectangleBorder(radius=6),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
customer_control = ft.Text(
|
customer_control = ft.Text(
|
||||||
|
|
@ -1584,42 +2028,6 @@ class FlutterStyleDashboard:
|
||||||
weight=ft.FontWeight.BOLD,
|
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_time_row = ft.Row(
|
||||||
[
|
[
|
||||||
date_button if date_button else ft.Text(
|
date_button if date_button else ft.Text(
|
||||||
|
|
@ -1633,7 +2041,7 @@ class FlutterStyleDashboard:
|
||||||
color=ft.Colors.BLUE_GREY_600,
|
color=ft.Colors.BLUE_GREY_600,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
spacing=8,
|
spacing=12,
|
||||||
)
|
)
|
||||||
|
|
||||||
customer_block = ft.Column(
|
customer_block = ft.Column(
|
||||||
|
|
@ -1652,17 +2060,7 @@ class FlutterStyleDashboard:
|
||||||
[
|
[
|
||||||
ft.Row(
|
ft.Row(
|
||||||
[
|
[
|
||||||
ft.Container(
|
doc_type_control,
|
||||||
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,
|
|
||||||
),
|
|
||||||
ft.Text(
|
ft.Text(
|
||||||
f"No: {self.editing_invoice.invoice_number}",
|
f"No: {self.editing_invoice.invoice_number}",
|
||||||
size=10,
|
size=10,
|
||||||
|
|
@ -1701,14 +2099,26 @@ class FlutterStyleDashboard:
|
||||||
[
|
[
|
||||||
ft.Text("明細", size=13, weight=ft.FontWeight.BOLD),
|
ft.Text("明細", size=13, weight=ft.FontWeight.BOLD),
|
||||||
ft.Container(expand=True),
|
ft.Container(expand=True),
|
||||||
ft.IconButton(
|
ft.Button(
|
||||||
ft.Icons.ADD_CIRCLE_OUTLINE,
|
content=ft.Row([
|
||||||
tooltip="行を追加",
|
ft.Icon(ft.Icons.SHUFFLE, size=16, color=ft.Colors.BLUE_GREY_700),
|
||||||
icon_color=ft.Colors.GREEN_600,
|
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,
|
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(),
|
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(),
|
) if not is_locked and not is_view_mode else ft.Container(),
|
||||||
],
|
],
|
||||||
|
spacing=8,
|
||||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
),
|
),
|
||||||
ft.Container(
|
ft.Container(
|
||||||
|
|
@ -1734,13 +2144,10 @@ class FlutterStyleDashboard:
|
||||||
[
|
[
|
||||||
ft.Button(
|
ft.Button(
|
||||||
content=ft.Row([
|
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),
|
ft.Text("PDF生成", size=12),
|
||||||
], spacing=6),
|
], spacing=6),
|
||||||
style=ft.ButtonStyle(
|
style=self.edit_button_style,
|
||||||
bgcolor=ft.Colors.BLUE_600,
|
|
||||||
color=ft.Colors.WHITE,
|
|
||||||
),
|
|
||||||
on_click=lambda _: self.generate_pdf_from_edit(),
|
on_click=lambda _: self.generate_pdf_from_edit(),
|
||||||
disabled=is_locked,
|
disabled=is_locked,
|
||||||
) if not is_locked else ft.Container(),
|
) if not is_locked else ft.Container(),
|
||||||
|
|
@ -1942,7 +2349,7 @@ class FlutterStyleDashboard:
|
||||||
products=self.app_service.product.get_all_products(),
|
products=self.app_service.product.get_all_products(),
|
||||||
on_product_select=self._open_product_picker_for_row,
|
on_product_select=self._open_product_picker_for_row,
|
||||||
row_refs=self._ensure_item_row_refs(),
|
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),
|
on_reorder=lambda old, new: self._reorder_item_row(old, new),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -2154,6 +2561,101 @@ class FlutterStyleDashboard:
|
||||||
self.page.update()
|
self.page.update()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"toast hide failed: {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:
|
def create_new_customer_screen(self) -> ft.Container:
|
||||||
"""新規/既存顧客登録・編集画面"""
|
"""新規/既存顧客登録・編集画面"""
|
||||||
editing_customer = getattr(self, "editing_customer_for_form", None)
|
editing_customer = getattr(self, "editing_customer_for_form", None)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue