This commit is contained in:
joe 2026-02-20 23:24:01 +09:00
parent 85263ebbf6
commit 97f2843620
170 changed files with 16665 additions and 11 deletions

306
app.log
View file

@ -1781,3 +1781,309 @@ Traceback (most recent call last):
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 33, in signal_handler
sys.exit(0)
SystemExit: 0
2026-02-19 22:51:46,592 - INFO - ショートカットキー設定完了
2026-02-19 22:51:46,629 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 22:51:53,673 - INFO - 機能選択: ダッシュボード
2026-02-19 22:51:55,722 - INFO - ダッシュボード機能実行
2026-02-19 22:51:57,871 - INFO - データベース初期化完了
2026-02-19 22:51:57,872 - ERROR - アプリケーション起動エラー: property 'page' of 'DashboardView' object has no setter
2026-02-19 22:52:04,395 - INFO - Session was garbage collected: r32T7sUcSbL2SDYQ
2026-02-19 22:52:04,400 - INFO - アプリケーション終了処理開始
2026-02-19 22:52:04,401 - INFO - アプリケーション正常終了
2026-02-19 22:52:05,803 - INFO - 機能選択: 売上管理
2026-02-19 22:52:07,041 - INFO - 売上管理機能実行
2026-02-19 22:52:09,192 - INFO - データベース初期化完了
2026-02-19 22:52:09,205 - INFO - アプリケーション起動完了
2026-02-19 22:52:14,741 - INFO - Session was garbage collected: 0mwG5U54BeEpEU0f
2026-02-19 22:52:14,747 - INFO - アプリケーション正常終了
2026-02-19 22:52:16,033 - INFO - アプリケーション正常終了
2026-02-19 22:52:16,034 - INFO - Session was garbage collected: cqadZf8CJor0nHkL
2026-02-19 22:52:16,090 - ERROR - Task exception was never retrieved
future: <Task finished name='Task-19' coro=<Session.dispatch_event() done, defined at /home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py:186> exception=SystemExit(0)>
Traceback (most recent call last):
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 392, in <module>
ft.run(main)
File "/home/user/.venv/lib/python3.12/site-packages/flet/app.py", line 96, in run
return asyncio.run(
^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/base_events.py", line 674, in run_until_complete
self.run_forever()
File "/usr/lib/python3.12/asyncio/base_events.py", line 641, in run_forever
self._run_once()
File "/usr/lib/python3.12/asyncio/base_events.py", line 1987, in _run_once
handle._run()
File "/usr/lib/python3.12/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *self._args)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 198, in dispatch_event
await control._trigger_event(event_name, event_data)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/base_control.py", line 346, in _trigger_event
await session.after_event(session.index.get(self._i))
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 263, in after_event
await self.__auto_update(control)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 275, in __auto_update
control.update()
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/page.py", line 452, in update
self.__update(self)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/page.py", line 461, in __update
self.session.patch_control(c)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 125, in patch_control
patch, added_controls, removed_controls = self.__get_update_control_patch(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 301, in __get_update_control_patch
patch, added_controls, removed_controls = ObjectPatch.from_diff(
^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 245, in from_diff
builder._compare_values(parent, path or [], None, src, dst, frozen=frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1049, in _compare_values
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 730, in _compare_lists
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 617, in _compare_lists
logger.debug(f"\n_compare_lists: {path} {src} {dst}")
^^^^^
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 33, in signal_handler
sys.exit(0)
SystemExit: 0
2026-02-19 23:00:53,888 - INFO - Session was garbage collected: 9p1HxBz0GLA4C782
2026-02-19 23:00:53,965 - INFO - アプリケーション終了処理開始
2026-02-19 23:00:53,966 - INFO - アプリケーション正常終了
2026-02-19 23:22:59,072 - INFO - ショートカットキー設定完了
2026-02-19 23:22:59,161 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:23:35,173 - INFO - 機能選択: 売上管理
2026-02-19 23:23:36,643 - INFO - 売上管理機能実行
2026-02-19 23:23:43,733 - INFO - データベース初期化完了
2026-02-19 23:23:43,750 - INFO - アプリケーション起動完了
2026-02-19 23:23:48,094 - INFO - Session was garbage collected: qYhLxUqfpSTEnvox
2026-02-19 23:23:48,141 - INFO - アプリケーション正常終了
2026-02-19 23:23:50,167 - INFO - Session was garbage collected: TggfJsBi5aMHHdWO
2026-02-19 23:24:47,303 - INFO - ショートカットキー設定完了
2026-02-19 23:24:47,414 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:26:42,048 - INFO - 機能選択: 売上管理
2026-02-19 23:26:43,693 - INFO - 機能選択: ダッシュボード
2026-02-19 23:26:45,628 - INFO - 機能選択: 商品管理
2026-02-19 23:26:46,375 - INFO - 機能選択: 伝票入力
2026-02-19 23:26:49,463 - INFO - アプリケーション正常終了
2026-02-19 23:26:49,465 - INFO - Session was garbage collected: PH8zQyLIFa6vmCCQ
2026-02-19 23:26:49,529 - ERROR - Task exception was never retrieved
future: <Task finished name='Task-18' coro=<Session.dispatch_event() done, defined at /home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py:186> exception=SystemExit(0)>
Traceback (most recent call last):
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 395, in <module>
ft.run(main)
File "/home/user/.venv/lib/python3.12/site-packages/flet/app.py", line 96, in run
return asyncio.run(
^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/base_events.py", line 674, in run_until_complete
self.run_forever()
File "/usr/lib/python3.12/asyncio/base_events.py", line 641, in run_forever
self._run_once()
File "/usr/lib/python3.12/asyncio/base_events.py", line 1987, in _run_once
handle._run()
File "/usr/lib/python3.12/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *self._args)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 198, in dispatch_event
await control._trigger_event(event_name, event_data)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/base_control.py", line 346, in _trigger_event
await session.after_event(session.index.get(self._i))
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 263, in after_event
await self.__auto_update(control)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 275, in __auto_update
control.update()
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/page.py", line 452, in update
self.__update(self)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/page.py", line 461, in __update
self.session.patch_control(c)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 125, in patch_control
patch, added_controls, removed_controls = self.__get_update_control_patch(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 301, in __get_update_control_patch
patch, added_controls, removed_controls = ObjectPatch.from_diff(
^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 245, in from_diff
builder._compare_values(parent, path or [], None, src, dst, frozen=frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1049, in _compare_values
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 730, in _compare_lists
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 730, in _compare_lists
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 990, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1049, in _compare_values
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 730, in _compare_lists
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 990, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1049, in _compare_values
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 990, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1049, in _compare_values
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 730, in _compare_lists
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 990, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1049, in _compare_values
self._compare_dataclasses(
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 964, in _compare_dataclasses
self._compare_values(dst, path, field_name, old, new, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 1033, in _compare_values
self._compare_lists(parent, _path_join(path, key), src, dst, frozen)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/object_patch.py", line 617, in _compare_lists
logger.debug(f"\n_compare_lists: {path} {src} {dst}")
^^^^^
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper
result = user_function(self)
^^^^^^^^^^^^^^^^^^^
File "<string>", line 3, in __repr__
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 33, in signal_handler
sys.exit(0)
SystemExit: 0
2026-02-19 23:28:56,404 - INFO - ショートカットキー設定完了
2026-02-19 23:28:56,436 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:30:48,833 - INFO - アプリケーション正常終了
2026-02-19 23:30:48,899 - INFO - Session was garbage collected: Cw7TxonDjbYMdw3T
2026-02-19 23:30:52,823 - INFO - ショートカットキー設定完了
2026-02-19 23:30:52,890 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:31:10,197 - INFO - アプリケーション正常終了
2026-02-19 23:31:10,226 - INFO - Session was garbage collected: c53GuDGBZXxvjCww
2026-02-19 23:31:14,680 - INFO - ショートカットキー設定完了
2026-02-19 23:31:14,714 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:31:47,781 - INFO - Session was garbage collected: WUNGebz2CNzU3cFk
2026-02-19 23:31:47,807 - INFO - アプリケーション正常終了
2026-02-19 23:32:09,911 - INFO - ショートカットキー設定完了
2026-02-19 23:32:09,950 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:32:28,407 - INFO - アプリケーション正常終了
2026-02-19 23:32:28,442 - INFO - Session was garbage collected: SnkMhUwRPwp6SFAp
2026-02-19 23:32:47,803 - INFO - ショートカットキー設定完了
2026-02-19 23:32:47,845 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:33:18,043 - INFO - Session was garbage collected: 74npbPaHkmm9OMOi
2026-02-19 23:33:18,047 - INFO - アプリケーション正常終了
2026-02-19 23:34:40,768 - INFO - ショートカットキー設定完了
2026-02-19 23:34:40,811 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:34:52,007 - INFO - 機能選択: ダッシュボード
2026-02-19 23:34:52,675 - INFO - 機能選択: 売上管理
2026-02-19 23:34:53,457 - INFO - 機能選択: ダッシュボード
2026-02-19 23:34:53,933 - INFO - 機能選択: 売上管理
2026-02-19 23:35:55,641 - INFO - Session was garbage collected: olQxcqxDARi4pgzu
2026-02-19 23:35:55,645 - INFO - アプリケーション正常終了
2026-02-19 23:36:22,945 - INFO - ショートカットキー設定完了
2026-02-19 23:36:22,987 - INFO - Compiz対応ショートカットキーアプリ起動完了
2026-02-19 23:37:21,006 - INFO - アプリケーション正常終了
2026-02-19 23:37:21,010 - INFO - Session was garbage collected: lJxOABS9XchSkkyZ
2026-02-19 23:37:21,085 - ERROR - Task exception was never retrieved
future: <Task finished name='Task-11' coro=<Session.dispatch_event() done, defined at /home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py:186> exception=SystemExit(0)>
Traceback (most recent call last):
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 395, in <module>
ft.run(main)
File "/home/user/.venv/lib/python3.12/site-packages/flet/app.py", line 96, in run
return asyncio.run(
^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/base_events.py", line 674, in run_until_complete
self.run_forever()
File "/usr/lib/python3.12/asyncio/base_events.py", line 641, in run_forever
self._run_once()
File "/usr/lib/python3.12/asyncio/base_events.py", line 1987, in _run_once
handle._run()
File "/usr/lib/python3.12/asyncio/events.py", line 88, in _run
self._context.run(self._callback, *self._args)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 198, in dispatch_event
await control._trigger_event(event_name, event_data)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/base_control.py", line 346, in _trigger_event
await session.after_event(session.index.get(self._i))
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 263, in after_event
await self.__auto_update(control)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 275, in __auto_update
control.update()
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/page.py", line 452, in update
self.__update(self)
File "/home/user/.venv/lib/python3.12/site-packages/flet/controls/page.py", line 461, in __update
self.session.patch_control(c)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 143, in patch_control
self.__send_message(
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/session.py", line 286, in __send_message
self.__conn.send_message(message)
File "/home/user/.venv/lib/python3.12/site-packages/flet/messaging/flet_socket_server.py", line 305, in send_message
m = msgpack.packb(
^^^^^^^^^^^^^^
File "/home/user/.venv/lib/python3.12/site-packages/msgpack/__init__.py", line 36, in packb
return Packer(**kwargs).pack(o)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 33, in signal_handler
sys.exit(0)
SystemExit: 0

View file

@ -37,12 +37,19 @@ class CompizShortcutsApp:
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# ウィンドウ設定
page.title = "Compiz対応ショートカットキー"
page.window_width = 800
page.window_height = 600
# ウィンドウ設定 - スマホ固定サイズ
page.title = "販売アシスト・ショートカットランチャー"
page.window.width = 420 # 400px → 420pxに拡大
page.window.height = 900 # 800px → 900pxに拡大して下切れ対策
page.window.resizable = False # 固定サイズ
page.window_center = True # 中央配置
page.theme_mode = ft.ThemeMode.LIGHT
# デバッグ表示
print(f"ウィンドウサイズ設定: {page.window.width} x {page.window.height}")
print(f"リサイズ可能: {page.window.resizable}")
print(f"中央配置: {page.window_center}")
# ウィンドウクローズイベント
page.on_window_close = lambda _: signal_handler(0, None)
@ -248,7 +255,7 @@ class CompizShortcutsApp:
content=self.main_container,
padding=20,
bgcolor=ft.Colors.GREY_100,
expand=True
width=380 # 360px → 380pxに拡大
)
)
@ -272,16 +279,16 @@ class CompizShortcutsApp:
),
ft.Container(
content=ft.Column([
ft.Text(title, size=18, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900),
ft.Text(description, size=14, color=ft.Colors.GREY_600)
ft.Text(title, size=16, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900),
ft.Text(description, size=12, color=ft.Colors.GREY_600)
], spacing=3),
width=350,
padding=ft.Padding.symmetric(horizontal=15, vertical=10),
width=280, # 280px → 300pxに調整
padding=ft.Padding.symmetric(horizontal=10, vertical=8),
bgcolor=ft.Colors.GREY_50,
border_radius=10,
margin=ft.Margin.only(left=10)
margin=ft.Margin.only(left=8)
)
], spacing=0),
], spacing=0, width=340), # Rowに幅制限を追加
margin=ft.Margin.only(bottom=10),
on_click=lambda _: self.show_function(title, description)
)

333
app_dashboard_template.py Normal file
View file

@ -0,0 +1,333 @@
"""
販売アシストダッシュボードテンプレート
統合ハブとして全機能へのアクセスを提供
"""
import flet as ft
import sqlite3
import signal
import sys
import logging
from datetime import datetime
from typing import List, Dict, Optional
# ロギング設定
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class DashboardTemplate:
"""ダッシュボードテンプレート"""
def __init__(self, page: ft.Page):
self.page = page
self.setup_page()
self.setup_database()
self.setup_ui()
def setup_page(self):
"""ページ設定"""
self.page.title = "販売アシスト・ダッシュボード"
self.page.window.width = 420
self.page.window.height = 900
self.page.window.resizable = False
self.page.window_center = True
self.page.theme_mode = ft.ThemeMode.LIGHT
# シグナルハンドラ
def signal_handler(signum, frame):
logging.info("アプリケーション正常終了")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def setup_database(self):
"""データベース初期化"""
try:
self.conn = sqlite3.connect('sales_assist.db')
self.cursor = self.conn.cursor()
# テーブル作成(必要に応じて)
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS sales_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
amount REAL,
product TEXT,
customer TEXT
)
''')
self.conn.commit()
logging.info("データベース接続完了")
except Exception as e:
logging.error(f"データベースエラー: {e}")
self.conn = None
def setup_ui(self):
"""UI構築"""
# ヘッダー
header = ft.Container(
content=ft.Column([
ft.Text(
"🏠 販売アシスト",
size=24,
weight=ft.FontWeight.BOLD,
color=ft.Colors.WHITE,
text_align=ft.TextAlign.CENTER
),
ft.Text(
f"今日: {datetime.now().strftime('%m/%d %H:%M')}",
size=14,
color=ft.Colors.WHITE,
text_align=ft.TextAlign.CENTER
)
], spacing=5),
padding=20,
bgcolor=ft.Colors.BLUE_600,
border_radius=15,
margin=ft.Margin.only(bottom=20)
)
# ツールバー(コンパクト)
toolbar = ft.Container(
content=ft.Row([
ft.IconButton(ft.Icons.ADD, tooltip="新規下書き", icon_size=20),
ft.IconButton(ft.Icons.SEARCH, tooltip="検索", icon_size=20),
ft.IconButton(ft.Icons.FILTER_LIST, tooltip="フィルター", icon_size=20),
ft.Container(expand=True),
ft.IconButton(ft.Icons.SYNC, tooltip="更新", icon_size=20),
], spacing=5),
padding=10,
bgcolor=ft.Colors.BLUE_50
)
# 検索バー
search_bar = ft.Container(
content=ft.TextField(
hint_text="下書き・顧客・商品を検索...",
prefix_icon=ft.Icons.SEARCH,
filled=True,
dense=True
),
padding=ft.Padding.symmetric(horizontal=15, vertical=5)
)
# メイン機能グリッド(参考デザイン風)
main_grid = ft.Container(
content=ft.Column([
ft.Text("🏠 メニュー", size=16, weight=ft.FontWeight.BOLD),
ft.GridView(
runs_count=2, # 2列グリッド
spacing=10,
run_spacing=10,
controls=[
self.create_grid_card("💰", "売上", "売上管理", ft.Colors.GREEN, "app_simple_working.py"),
self.create_grid_card("👥", "顧客", "顧客管理", ft.Colors.ORANGE, "app_master_management.py"),
self.create_grid_card("📦", "商品", "商品管理", ft.Colors.PURPLE, "app_hierarchical_product_master.py"),
self.create_grid_card("📋", "伝票", "伝票管理", ft.Colors.RED, "app_slip_framework_demo.py"),
self.create_grid_card("🔍", "検索", "伝票検索", ft.Colors.BLUE, "app_slip_explorer.py"),
self.create_grid_card("📱", "操作", "インタラクティブ", ft.Colors.CYAN, "app_slip_interactive.py"),
]
)
], spacing=10),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=15,
shadow=ft.BoxShadow(
spread_radius=1,
blur_radius=5,
color=ft.Colors.GREY_300,
offset=ft.Offset(0, 2)
),
margin=ft.Margin.only(bottom=15)
)
# 下書き一覧(コンパクト)
draft_section = ft.Container(
content=ft.Column([
ft.Text("📝 最近の下書き", size=16, weight=ft.FontWeight.BOLD),
ft.Divider(height=1),
self.create_draft_item("売上メモ", "顧客: 田中様", "2分前"),
self.create_draft_item("商品リスト", "新商品10件", "1時間前"),
ft.Container(
content=ft.Button(" もっと見る", bgcolor=ft.Colors.BLUE_50, color=ft.Colors.BLUE_700),
alignment=ft.alignment.Alignment(0, 0)
)
], spacing=8),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=15,
margin=ft.Margin.only(bottom=15)
)
# 設定セクション
settings_section = ft.Container(
content=ft.Row([
ft.IconButton(ft.Icons.SETTINGS, icon_size=20),
ft.Text("設定", size=14),
ft.Container(expand=True),
ft.Icon(ft.Icons.CHEVRON_RIGHT, size=16, color=ft.Colors.GREY_400)
]),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=10,
border=ft.Border.all(1, ft.Colors.GREY_200)
)
# 状態サマリー
status_summary = ft.Container(
content=ft.Column([
ft.Text("📈 状態サマリー", size=18, weight=ft.FontWeight.BOLD),
ft.Divider(height=1, thickness=1),
self.get_status_info()
], spacing=10),
padding=20,
bgcolor=ft.Colors.BLUE_50,
border_radius=15,
margin=ft.Margin.only(bottom=20)
)
# Debug情報
debug_info = ft.Container(
content=ft.Column([
ft.Text("🔧 Debug情報", size=16, weight=ft.FontWeight.BOLD),
ft.Text(f"DB接続: {'' if self.conn else ''}", size=12),
ft.Text(f"ウィンドウ: 420x900", size=12),
ft.Text(f"テーマ: ライト", size=12),
], spacing=5),
padding=15,
bgcolor=ft.Colors.GREY_100,
border_radius=10
)
# メインコンテナ(再構成)
self.main_container = ft.Column([
header,
search_bar,
main_grid,
draft_section,
settings_section
], spacing=5, scroll=ft.ScrollMode.AUTO)
# ページに追加
self.page.add(
ft.Container(
content=self.main_container,
padding=10, # 20 → 10に縮小
bgcolor=ft.Colors.GREY_50,
expand=True
)
)
def create_grid_card(self, icon: str, title: str, subtitle: str, color: ft.Colors, app_file: str) -> ft.Container:
"""グリッドカード作成(参考デザイン風)"""
return ft.Container(
content=ft.Column([
ft.Container(
content=ft.Text(icon, size=32),
width=60,
height=60,
bgcolor=color,
alignment=ft.alignment.Alignment(0, 0),
border_radius=15,
margin=ft.Margin.only(bottom=10)
),
ft.Text(title, size=14, weight=ft.FontWeight.BOLD, text_align=ft.TextAlign.CENTER),
ft.Text(subtitle, size=10, color=ft.Colors.GREY_600, text_align=ft.TextAlign.CENTER)
], spacing=5),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=15,
border=ft.Border.all(1, ft.Colors.GREY_200),
on_click=lambda _: self.launch_app(app_file, title)
)
def create_draft_item(self, title: str, description: str, time: str) -> ft.Container:
"""下書きアイテム作成"""
return ft.Container(
content=ft.Row([
ft.Column([
ft.Text(title, size=14, weight=ft.FontWeight.BOLD),
ft.Text(description, size=12, color=ft.Colors.GREY_600)
], expand=True),
ft.Text(time, size=10, color=ft.Colors.GREY_500)
], spacing=10),
padding=10,
bgcolor=ft.Colors.GREY_50,
border_radius=8,
border=ft.Border.all(1, ft.Colors.GREY_200)
)
def get_status_info(self) -> ft.Column:
"""状態情報取得"""
if not self.conn:
return ft.Column([
ft.Text("データベース未接続", size=12, color=ft.Colors.RED),
ft.Text("機能制限中", size=12, color=ft.Colors.ORANGE)
], spacing=5)
try:
# 今日の売上件数
today = datetime.now().strftime('%Y-%m-%d')
self.cursor.execute("SELECT COUNT(*) FROM sales_log WHERE date = ?", (today,))
today_sales = self.cursor.fetchone()[0]
return ft.Column([
ft.Text(f"今日の売上: {today_sales}", size=14, color=ft.Colors.BLUE_700),
ft.Text("システム状態: 正常", size=14, color=ft.Colors.GREEN_600),
ft.Text("最終更新: 剛剣", size=12, color=ft.Colors.GREY_600)
], spacing=5)
except Exception as e:
return ft.Column([
ft.Text("状態取得エラー", size=12, color=ft.Colors.RED),
ft.Text(str(e), size=10, color=ft.Colors.GREY_600)
], spacing=5)
def launch_app(self, app_file: str, app_name: str):
"""アプリ起動"""
import subprocess
try:
# SnackBarで通知
self.page.snack_bar = ft.SnackBar(
content=ft.Text(f"{app_name}を起動中..."),
bgcolor=ft.Colors.BLUE
)
self.page.snack_bar.open = True
self.page.update()
# サブプロセスで起動
subprocess.Popen(['python', app_file])
logging.info(f"{app_name}起動: {app_file}")
except Exception as e:
logging.error(f"{app_name}起動エラー: {e}")
self.page.snack_bar = ft.SnackBar(
content=ft.Text(f"起動エラー: {e}"),
bgcolor=ft.Colors.RED
)
self.page.snack_bar.open = True
self.page.update()
def main(page: ft.Page):
"""メイン関数"""
try:
dashboard = DashboardTemplate(page)
logging.info("ダッシュボード起動完了")
except Exception as e:
logging.error(f"ダッシュボード起動エラー: {e}")
page.snack_bar = ft.SnackBar(
content=ft.Text(f"起動エラー: {e}"),
bgcolor=ft.Colors.RED
)
page.snack_bar.open = True
page.update()
if __name__ == "__main__":
ft.run(main)

View file

@ -0,0 +1,547 @@
"""
Flutter風ダッシュボード
下部ナビゲーションと洗練されたUIコンポーネントを実装
"""
import flet as ft
import signal
import sys
import logging
from datetime import datetime
from typing import List, Dict, Optional
from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer
from components.customer_picker import CustomerPickerModal
from services.app_service import AppService
# ロギング設定
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class FlutterStyleDashboard:
"""Flutter風の統合ダッシュボード"""
def __init__(self, page: ft.Page):
self.page = page
self.current_tab = 0 # 0: 新規作成, 1: 発行履歴
self.selected_customer = None
self.selected_document_type = DocumentType.INVOICE
self.amount_value = "250000"
self.customer_picker = None
self.is_customer_picker_open = False
self.customer_search_query = ""
self.show_offsets = False
# ビジネスロジックサービス
self.app_service = AppService()
self.invoices = []
self.customers = []
self.setup_page()
self.setup_database()
self.setup_ui()
def setup_page(self):
"""ページ設定"""
self.page.title = "販売アシスト1号"
self.page.window.width = 420
self.page.window.height = 900
self.page.theme_mode = ft.ThemeMode.LIGHT
# Fletのライフサイクルに任せるSystemExitがasyncioに伝播して警告になりやすい
def setup_database(self):
"""データ初期化(サービス層経由)"""
try:
# 顧客データ読み込み
self.customers = self.app_service.customer.get_all_customers()
# 伝票データ読み込み
self.invoices = self.app_service.invoice.get_recent_invoices(20)
logging.info(f"データ初期化: 顧客{len(self.customers)}件, 伝票{len(self.invoices)}")
except Exception as e:
logging.error(f"データ初期化エラー: {e}")
def create_sample_data(self):
"""サンプル伝票データ作成"""
try:
# サンプルデータ
sample_invoices = create_sample_invoices()
for invoice in sample_invoices:
self.cursor.execute('''
INSERT OR REPLACE INTO slips
(document_type, customer_name, amount, date, status, description)
VALUES (?, ?, ?, ?, ?, ?)
''', (
invoice.document_type.value,
invoice.customer.formal_name,
invoice.total_amount,
invoice.date.strftime('%Y-%m-%d'),
'完了',
invoice.notes
))
self.conn.commit()
except Exception as e:
logging.error(f"サンプルデータ作成エラー: {e}")
def setup_ui(self):
"""UIセットアップ"""
# 下部ナビゲーションNavigationRail風
bottom_nav = ft.Container(
content=ft.Row([
ft.Container(
content=ft.IconButton(
ft.Icons.ADD_BOX,
selected=self.current_tab == 0,
on_click=lambda _: self.on_tab_change(0),
),
),
ft.Text("新規作成"),
ft.Container(
content=ft.IconButton(
ft.Icons.HISTORY,
selected=self.current_tab == 1,
on_click=lambda _: self.on_tab_change(1),
),
),
ft.Text("発行履歴"),
], alignment=ft.MainAxisAlignment.CENTER),
padding=ft.Padding.all(10),
bgcolor=ft.Colors.BLUE_GREY_50,
)
# メインコンテンツ
self.main_content = ft.Column([], expand=True)
# ページ構成
self.page.add(
ft.Column([
self.main_content,
bottom_nav,
], expand=True)
)
# 初期表示
self.update_main_content()
def on_tab_change(self, index):
"""タブ切り替え"""
self.current_tab = index
self.update_main_content()
self.page.update()
def update_main_content(self):
"""メインコンテンツ更新"""
self.main_content.controls.clear()
if self.current_tab == 0:
if self.is_customer_picker_open:
self.main_content.controls.append(self.create_customer_picker_screen())
else:
# 新規作成画面
self.main_content.controls.append(self.create_slip_input_screen())
else:
# 発行履歴画面
self.main_content.controls.append(self.create_slip_history_screen())
self.page.update()
def create_customer_picker_screen(self) -> ft.Container:
"""顧客選択画面(画面内遷移・ダイアログ不使用)"""
self.customers = self.app_service.customer.get_all_customers()
def back(_=None):
self.is_customer_picker_open = False
self.customer_search_query = ""
self.update_main_content()
list_container = ft.Column([], spacing=0, scroll=ft.ScrollMode.AUTO, expand=True)
def render_list(customers: List[Customer]):
list_container.controls.clear()
for customer in customers:
list_container.controls.append(
ft.ListTile(
title=ft.Text(customer.formal_name, weight=ft.FontWeight.BOLD),
subtitle=ft.Text(f"{customer.address}\n{customer.phone}"),
on_click=lambda _, c=customer: select_customer(c),
)
)
self.page.update()
def select_customer(customer: Customer):
self.selected_customer = customer
logging.info(f"顧客を選択: {customer.formal_name}")
back()
def on_search_change(e):
q = (e.control.value or "").strip().lower()
self.customer_search_query = q
if not q:
render_list(self.customers)
return
filtered = [
c
for c in self.customers
if q in (c.name or "").lower()
or q in (c.formal_name or "").lower()
or q in (c.address or "").lower()
or q in (c.phone or "").lower()
]
render_list(filtered)
search_field = ft.TextField(
label="顧客検索",
prefix_icon=ft.Icons.SEARCH,
value=self.customer_search_query,
on_change=on_search_change,
autofocus=True,
)
header = ft.Container(
content=ft.Row(
[
ft.IconButton(ft.Icons.ARROW_BACK, on_click=back),
ft.Text("顧客を選択", size=18, weight=ft.FontWeight.BOLD),
]
),
padding=ft.padding.all(15),
bgcolor=ft.Colors.BLUE_GREY,
)
content = ft.Column(
[
header,
ft.Container(content=search_field, padding=ft.padding.all(15)),
ft.Container(content=list_container, padding=ft.padding.symmetric(horizontal=15), expand=True),
],
expand=True,
)
# 初期表示
render_list(self.customers)
return ft.Container(content=content, expand=True)
def create_slip_input_screen(self) -> ft.Container:
"""伝票入力画面"""
# 帳票種類選択(タブ風ボタン)
document_types = list(DocumentType)
type_buttons = ft.Row([
ft.Container(
content=ft.Button(
content=ft.Text(doc_type.value, size=12),
bgcolor=ft.Colors.BLUE_GREY_800 if i == 0 else ft.Colors.GREY_300,
color=ft.Colors.WHITE if i == 0 else ft.Colors.BLACK,
on_click=lambda _, idx=i, dt=doc_type: self.select_document_type(dt.value),
width=100,
height=45,
),
margin=ft.margin.only(right=5),
) for i, doc_type in enumerate(document_types)
], wrap=True)
return ft.Container(
content=ft.Column([
# ヘッダー
ft.Container(
content=ft.Row([
ft.Text("📋 販売アシスト1号", size=20, weight=ft.FontWeight.BOLD),
ft.Container(expand=True),
ft.IconButton(ft.Icons.SETTINGS, icon_size=20),
]),
padding=ft.padding.all(15),
bgcolor=ft.Colors.BLUE_GREY,
),
# 帳票種類選択
ft.Container(
content=ft.Column([
ft.Text("帳票の種類を選択", size=16, weight=ft.FontWeight.BOLD),
ft.Container(height=10),
type_buttons,
]),
padding=ft.padding.all(20),
),
# 顧客選択
ft.Container(
content=ft.Column([
ft.Text("宛先と基本金額の設定", size=16, weight=ft.FontWeight.BOLD),
ft.Container(height=10),
ft.Row(
[
ft.TextField(
label="取引先名",
value=self.selected_customer.formal_name if self.selected_customer else "未選択",
read_only=True,
border_color=ft.Colors.BLUE_GREY,
expand=True,
),
ft.IconButton(
ft.Icons.PERSON_SEARCH,
tooltip="顧客を選択",
on_click=self.open_customer_picker,
),
],
spacing=8,
),
ft.Container(height=10),
ft.TextField(
label="基本金額 (税抜)",
value=self.amount_value,
keyboard_type=ft.KeyboardType.NUMBER,
on_change=self.on_amount_change,
border_color=ft.Colors.BLUE_GREY,
),
]),
padding=ft.padding.all(20),
),
# 作成ボタン
ft.Container(
content=ft.Button(
content=ft.Row([
ft.Icon(ft.Icons.DESCRIPTION, color=ft.Colors.WHITE),
ft.Text("伝票を作成して詳細編集へ", color=ft.Colors.WHITE),
], alignment=ft.MainAxisAlignment.CENTER),
style=ft.ButtonStyle(
bgcolor=ft.Colors.BLUE_GREY_800,
padding=ft.padding.all(20),
shape=ft.RoundedRectangleBorder(radius=15),
),
width=400,
height=60,
on_click=self.create_slip,
),
padding=ft.padding.all(20),
),
]),
expand=True,
)
def create_slip_history_screen(self) -> ft.Container:
"""伝票履歴画面"""
# 履歴データ読み込み
slips = self.load_slips()
if not self.show_offsets:
slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))]
def on_toggle_offsets(e):
self.show_offsets = bool(e.control.value)
self.update_main_content()
# 履歴カードリスト
slip_cards = []
for slip in slips:
card = self.create_slip_card(slip)
slip_cards.append(card)
return ft.Container(
content=ft.Column([
# ヘッダー
ft.Container(
content=ft.Row([
ft.Text("📄 発行履歴管理", size=20, weight=ft.FontWeight.BOLD),
ft.Container(expand=True),
ft.Row(
[
ft.Text("赤伝を表示", size=12, color=ft.Colors.WHITE),
ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets),
],
spacing=5,
),
ft.IconButton(ft.Icons.CLEAR_ALL, icon_size=20),
]),
padding=ft.padding.all(15),
bgcolor=ft.Colors.BLUE_GREY,
),
# 履歴リスト
ft.Column(
controls=slip_cards,
spacing=10,
scroll=ft.ScrollMode.AUTO,
expand=True,
),
]),
expand=True,
)
def create_slip_card(self, slip) -> ft.Card:
"""伝票カード作成"""
# サービス層からは Invoice オブジェクトが返る
if isinstance(slip, Invoice):
slip_type = slip.document_type.value
customer_name = slip.customer.formal_name
amount = slip.total_amount
date = slip.date.strftime("%Y-%m-%d")
status = "赤伝" if getattr(slip, "is_offset", False) else "完了"
else:
slip_id, slip_type, customer_name, amount, date, status, description, created_at = slip
# タイプに応じたアイコンと色
type_config = {
"売上伝票": {"icon": "💰", "color": ft.Colors.GREEN},
"見積書": {"icon": "📄", "color": ft.Colors.BLUE},
"納品書": {"icon": "📦", "color": ft.Colors.PURPLE},
"請求書": {"icon": "📋", "color": ft.Colors.ORANGE},
"領収書": {"icon": "🧾", "color": ft.Colors.RED}
}
config = type_config.get(slip_type, {"icon": "📝", "color": ft.Colors.GREY})
def issue_offset(_=None):
if not isinstance(slip, Invoice):
return
if getattr(slip, "is_offset", False):
return
offset_invoice = self.app_service.invoice.create_offset_invoice(slip.uuid)
if offset_invoice and offset_invoice.file_path:
self.app_service.invoice.delete_pdf_file(offset_invoice.file_path)
# リストを更新
self.invoices = self.app_service.invoice.get_recent_invoices(20)
self.update_main_content()
actions_row = None
if isinstance(slip, Invoice) and not getattr(slip, "is_offset", False):
actions_row = ft.Row(
[
ft.IconButton(
ft.Icons.REPLAY_CIRCLE_FILLED,
tooltip="赤伝(相殺)を発行",
icon_color=ft.Colors.RED_400,
on_click=issue_offset,
)
],
alignment=ft.MainAxisAlignment.END,
)
display_amount = amount
if isinstance(slip, Invoice) and getattr(slip, "is_offset", False):
display_amount = -abs(amount)
return ft.Card(
content=ft.Container(
content=ft.Column([
ft.Row([
ft.Container(
content=ft.Text(config["icon"], size=24),
width=40,
height=40,
bgcolor=config["color"],
border_radius=20,
alignment=ft.alignment.Alignment(0, 0),
),
ft.Container(
content=ft.Column([
ft.Text(slip_type, size=12, weight=ft.FontWeight.BOLD),
ft.Text(f"{customer_name} ¥{display_amount:,.0f}", size=10),
]),
expand=True,
),
]),
ft.Container(height=5),
ft.Text(f"日付: {date}", size=10, color=ft.Colors.GREY_600),
ft.Text(f"状態: {status}", size=10, color=ft.Colors.GREY_600),
actions_row if actions_row else ft.Container(height=0),
]),
padding=ft.padding.all(15),
),
elevation=3,
)
def open_customer_picker(self, e=None):
"""顧客選択を開く(画面内遷移)"""
logging.info("顧客選択画面へ遷移")
self.is_customer_picker_open = True
self.update_main_content()
def on_customer_selected(self, customer: Customer):
"""顧客選択時の処理"""
self.selected_customer = customer
logging.info(f"顧客を選択: {customer.formal_name}")
# UIを更新して選択された顧客を表示
self.update_main_content()
def on_customer_deleted(self, customer: Customer):
"""顧客削除時の処理"""
self.app_service.customer.delete_customer(customer.id)
self.customers = self.app_service.customer.get_all_customers()
logging.info(f"顧客を削除: {customer.formal_name}")
# モーダルを再表示してリストを更新
if self.customer_picker and self.customer_picker.is_open:
self.customer_picker.update_customer_list(self.customers)
def on_amount_change(self, e):
"""金額変更時の処理"""
self.amount_value = e.control.value
logging.info(f"金額を変更: {self.amount_value}")
def on_document_type_change(self, index):
"""帳票種類変更"""
document_types = list(DocumentType)
selected_type = document_types[index]
logging.info(f"帳票種類を変更: {selected_type.value}")
# TODO: 選択された種類を保存
def select_document_type(self, doc_type: str):
"""帳票種類選択"""
# DocumentTypeから対応するenumを見つける
for dt in DocumentType:
if dt.value == doc_type:
self.selected_document_type = dt
logging.info(f"帳票種類を選択: {doc_type}")
break
def create_slip(self, e=None):
"""伝票作成 - サービス層を使用"""
if not self.selected_customer:
logging.warning("顧客が選択されていません")
return
try:
amount = int(self.amount_value) if self.amount_value else 250000
except ValueError:
amount = 250000
logging.info(f"伝票を作成: {self.selected_document_type.value}, {self.selected_customer.formal_name}, ¥{amount:,}")
# サービス層経由で伝票作成
invoice = self.app_service.invoice.create_invoice(
customer=self.selected_customer,
document_type=self.selected_document_type,
amount=amount,
notes=""
)
if invoice:
if invoice.file_path:
self.app_service.invoice.delete_pdf_file(invoice.file_path)
invoice.file_path = None
logging.info(f"伝票作成成功: {invoice.invoice_number}")
# リストを更新
self.invoices = self.app_service.invoice.get_recent_invoices(20)
# 発行履歴タブに切り替え
self.on_tab_change(1)
else:
logging.error("伝票作成失敗")
def load_slips(self) -> List[Invoice]:
"""伝票データ読み込み - サービス層経由"""
return self.app_service.invoice.get_recent_invoices(20)
def main(page: ft.Page):
"""メイン関数"""
app = FlutterStyleDashboard(page)
page.update()
if __name__ == "__main__":
ft.app(target=main)

172
app_slip_adaptive.py Normal file
View file

@ -0,0 +1,172 @@
"""
業態適応型伝票システム
事業者の業態に応じて最適なフォームを提供
"""
import flet as ft
import sqlite3
import signal
import sys
import logging
from datetime import datetime
class AdaptiveSlipSystem:
def __init__(self, page: ft.Page):
self.page = page
self.setup_page()
self.setup_database()
self.setup_ui()
def setup_page(self):
self.page.title = "業態適応伝票"
self.page.window.width = 420
self.page.window.height = 900
self.page.window.resizable = False
self.page.window_center = True
def setup_database(self):
try:
self.conn = sqlite3.connect('sales_assist.db')
self.cursor = self.conn.cursor()
# 業態マスター
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS business_types (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE,
slip_mode TEXT
)
''')
# サンプル業態
business_types = [
("小売店", "detail"), # 明細書モード
("配達業", "simple"), # 簡素モード
("サービス業", "detail"), # 明細書モード
("製造業", "detail"), # 明細書モード
]
for bt in business_types:
self.cursor.execute(
"INSERT OR IGNORE INTO business_types (name, slip_mode) VALUES (?, ?)", bt
)
self.conn.commit()
except Exception as e:
logging.error(f"DBエラー: {e}")
def setup_ui(self):
# 業態選択
self.business_type_dropdown = ft.Dropdown(
label="業態を選択",
options=[
ft.dropdown.Option("小売店"),
ft.dropdown.Option("配達業"),
ft.dropdown.Option("サービス業"),
ft.dropdown.Option("製造業"),
],
on_change=self.on_business_change
)
# 動的フォームコンテナ
self.form_container = ft.Container()
# メインレイアウト
self.page.add(
ft.Column([
ft.Text("🏢 業態適応伝票システム", size=20, weight=ft.FontWeight.BOLD),
self.business_type_dropdown,
self.form_container
], spacing=20)
)
def on_business_change(self, e):
business_type = e.control.value
self.load_adaptive_form(business_type)
def load_adaptive_form(self, business_type: str):
"""業態に応じたフォーム読み込み"""
if business_type == "配達業":
self.form_container.content = self.create_simple_form()
else:
self.form_container.content = self.create_detail_form()
self.page.update()
def create_simple_form(self):
"""簡素フォーム(配達業向け)"""
return ft.Container(
content=ft.Column([
ft.Text("⛽ 簡素伝票モード", size=16, weight=ft.FontWeight.BOLD),
ft.TextField(label="顧客名"),
ft.Row([
ft.TextField(label="数量", width=100),
ft.TextField(label="単価", width=100),
ft.TextField(label="金額", width=100, read_only=True)
]),
ft.TextField(label="配達先"),
ft.ElevatedButton("保存", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE)
], spacing=10),
padding=20,
bgcolor=ft.Colors.WHITE,
border_radius=10
)
def create_detail_form(self):
"""明細フォーム(小売店・サービス業向け)"""
return ft.Container(
content=ft.Column([
ft.Text("📋 明細伝票モード", size=16, weight=ft.FontWeight.BOLD),
ft.TextField(label="顧客名"),
# 明細テーブル
ft.DataTable(
columns=[
ft.DataColumn(ft.Text("商品名")),
ft.DataColumn(ft.Text("数量")),
ft.DataColumn(ft.Text("単価")),
ft.DataColumn(ft.Text("金額")),
],
rows=[
ft.DataRow(
cells=[
ft.DataCell(ft.TextField(hint_text="商品名")),
ft.DataCell(ft.TextField(hint_text="数量")),
ft.DataCell(ft.TextField(hint_text="単価")),
ft.DataCell(ft.TextField(hint_text="金額")),
]
)
]
),
ft.Row([
ft.Text("小計:", weight=ft.FontWeight.BOLD),
ft.TextField(label="小計", width=100, read_only=True)
]),
ft.Row([
ft.Text("税:", weight=ft.FontWeight.BOLD),
ft.TextField(label="消費税", width=100, read_only=True)
]),
ft.Row([
ft.Text("合計:", weight=ft.FontWeight.BOLD),
ft.TextField(label="合計", width=100, read_only=True)
]),
ft.ElevatedButton("保存", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE)
], spacing=10),
padding=20,
bgcolor=ft.Colors.WHITE,
border_radius=10
)
def main(page: ft.Page):
try:
app = AdaptiveSlipSystem(page)
logging.info("業態適応伝票システム起動")
except Exception as e:
logging.error(f"起動エラー: {e}")
if __name__ == "__main__":
ft.run(main)

416
app_slip_explorer.py Normal file
View file

@ -0,0 +1,416 @@
"""
伝票エクスプローラー
伝票の検索閲覧管理を直感的に行う
土地勘を持たせるための視覚的ナビゲーション
"""
import flet as ft
import sqlite3
import signal
import sys
import logging
from datetime import datetime, timedelta
from typing import List, Dict, Optional
# ロギング設定
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
class SlipExplorer:
"""伝票エクスプローラー"""
def __init__(self, page: ft.Page):
self.page = page
self.setup_page()
self.setup_database()
self.setup_ui()
def setup_page(self):
"""ページ設定"""
self.page.title = "伝票エクスプローラー"
self.page.window.width = 420
self.page.window.height = 900
self.page.window.resizable = False
self.page.window_center = True
self.page.theme_mode = ft.ThemeMode.LIGHT
# シグナルハンドラ
def signal_handler(signum, frame):
logging.info("伝票エクスプローラー正常終了")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def setup_database(self):
"""データベース初期化"""
try:
self.conn = sqlite3.connect('sales_assist.db')
self.cursor = self.conn.cursor()
# 伝票テーブル作成
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS slips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slip_type TEXT, -- 売上伝票見積書納品書請求書領収書
customer_name TEXT,
amount REAL,
date TEXT,
status TEXT, -- 下書き発行済入金済キャンセル
description TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
# サンプルデータ作成
self.create_sample_data()
logging.info("伝票データベース接続完了")
except Exception as e:
logging.error(f"データベースエラー: {e}")
self.conn = None
def create_sample_data(self):
"""サンプルデータ作成"""
try:
# サンプル伝票データ
sample_slips = [
("売上伝票", "田中商事", 50000, "2026-02-19", "発行済", "A商品100個"),
("見積書", "鈴木商店", 75000, "2026-02-18", "下書き", "B商品50個"),
("納品書", "伊藤工業", 120000, "2026-02-17", "発行済", "C製品20台"),
("請求書", "高橋建設", 200000, "2026-02-16", "入金済", "工事代金"),
("領収書", "渡辺商事", 80000, "2026-02-15", "発行済", "D商品40個"),
]
for slip in sample_slips:
self.cursor.execute('''
INSERT OR IGNORE INTO slips
(slip_type, customer_name, amount, date, status, description)
VALUES (?, ?, ?, ?, ?, ?)
''', slip)
self.conn.commit()
except Exception as e:
logging.error(f"サンプルデータ作成エラー: {e}")
def setup_ui(self):
"""UI構築"""
# ヘッダー
header = ft.Container(
content=ft.Column([
ft.Text(
"📋 伝票エクスプローラー",
size=20,
weight=ft.FontWeight.BOLD,
color=ft.Colors.WHITE,
text_align=ft.TextAlign.CENTER
),
ft.Text(
"伝票の検索・閲覧・管理",
size=14,
color=ft.Colors.WHITE,
text_align=ft.TextAlign.CENTER
)
], spacing=5),
padding=15,
bgcolor=ft.Colors.BLUE_600,
border_radius=15,
margin=ft.Margin.only(bottom=15)
)
# 検索バー
search_bar = ft.Container(
content=ft.Row([
ft.TextField(
hint_text="伝票を検索...",
prefix_icon=ft.Icons.SEARCH,
filled=True,
dense=True,
expand=True,
on_change=self.on_search_change
),
ft.IconButton(ft.Icons.FILTER_LIST, tooltip="フィルター", icon_size=20),
], spacing=5),
padding=ft.Padding.symmetric(horizontal=15, vertical=5),
margin=ft.Margin.only(bottom=15)
)
# フィルターパネル
filter_panel = ft.Container(
content=ft.Column([
ft.Text("🔍 フィルター", size=14, weight=ft.FontWeight.BOLD),
ft.Divider(height=1),
# 顧客フィルター(チェックボックス)
ft.Text("顧客", size=12, weight=ft.FontWeight.BOLD),
ft.Column([
self.create_checkbox_filter("田中商事", "customer"),
self.create_checkbox_filter("鈴木商店", "customer"),
self.create_checkbox_filter("伊藤工業", "customer"),
self.create_checkbox_filter("高橋建設", "customer"),
], spacing=5),
# 期間フィルター(ラジオボタン)
ft.Text("期間", size=12, weight=ft.FontWeight.BOLD),
ft.Column([
self.create_radio_filter("今日", "period", True),
self.create_radio_filter("今週", "period"),
self.create_radio_filter("今月", "period"),
self.create_radio_filter("全期間", "period"),
], spacing=5),
# 金額帯フィルター(チェックボックス)
ft.Text("金額帯", size=12, weight=ft.FontWeight.BOLD),
ft.Column([
self.create_checkbox_filter("0-1万円", "amount_0_1"),
self.create_checkbox_filter("1-5万円", "amount_1_5"),
self.create_checkbox_filter("5万円以上", "amount_5_plus"),
], spacing=5),
], spacing=10),
padding=15,
bgcolor=ft.Colors.WHITE,
border_radius=10,
margin=ft.Margin.only(bottom=15)
)
# 伝票一覧
self.slip_list = ft.Column([], spacing=8, scroll=ft.ScrollMode.AUTO)
# 統計情報
stats_container = ft.Container(
content=self.get_stats_info(),
padding=15,
bgcolor=ft.Colors.BLUE_50,
border_radius=10,
margin=ft.Margin.only(bottom=15)
)
# メインコンテナ
self.main_container = ft.Column([
header,
search_bar,
filter_panel,
stats_container,
ft.Container(
content=self.slip_list,
expand=True,
padding=ft.Padding.symmetric(horizontal=15)
)
], spacing=5)
# ページに追加
self.page.add(
ft.Container(
content=self.main_container,
padding=10,
bgcolor=ft.Colors.GREY_50,
expand=True
)
)
# 初期データ読み込み
self.load_slips()
def create_checkbox_filter(self, label: str, filter_type: str) -> ft.Row:
"""チェックボックスフィルター作成"""
checkbox = ft.Checkbox(label=label, value=False, on_change=lambda e: self.apply_filters())
return ft.Row([checkbox], spacing=0)
def create_radio_filter(self, label: str, filter_type: str, is_default: bool = False) -> ft.Row:
"""ラジオボタンフィルター作成"""
radio = ft.Radio(
value=label,
label=label,
on_change=lambda e: self.apply_filters()
)
return ft.Row([radio], spacing=0)
def apply_filters(self):
"""フィルター適用"""
# TODO: フィルターロジック実装
self.load_slips()
def get_stats_info(self) -> ft.Column:
"""統計情報取得"""
if not self.conn:
return ft.Column([
ft.Text("データベース未接続", size=12, color=ft.Colors.RED)
])
try:
# 各種統計
self.cursor.execute("SELECT COUNT(*) FROM slips")
total_slips = self.cursor.fetchone()[0]
self.cursor.execute("SELECT COUNT(*) FROM slips WHERE status = '下書き'")
draft_slips = self.cursor.fetchone()[0]
self.cursor.execute("SELECT COUNT(*) FROM slips WHERE status = '入金済'")
paid_slips = self.cursor.fetchone()[0]
self.cursor.execute("SELECT SUM(amount) FROM slips WHERE status = '入金済'")
total_amount = self.cursor.fetchone()[0] or 0
return ft.Column([
ft.Text("📊 伝票統計", size=14, weight=ft.FontWeight.BOLD),
ft.Row([
ft.Text(f"総数: {total_slips}", size=12, expand=True),
ft.Text(f"下書き: {draft_slips}", size=12, expand=True),
]),
ft.Row([
ft.Text(f"入金済: {paid_slips}", size=12, expand=True),
ft.Text(f"合計: ¥{total_amount:,.0f}", size=12, expand=True),
])
], spacing=5)
except Exception as e:
return ft.Column([
ft.Text("統計取得エラー", size=12, color=ft.Colors.RED)
])
def load_slips(self, filter_type: str = "all"):
"""伝票一覧読み込み"""
if not self.conn:
return
try:
query = "SELECT * FROM slips"
params = []
if filter_type != "all":
query += " WHERE slip_type = ?"
params = [filter_type]
query += " ORDER BY date DESC, created_at DESC"
self.cursor.execute(query, params)
slips = self.cursor.fetchall()
# 一覧をクリアして再構築
self.slip_list.controls.clear()
for slip in slips:
slip_item = self.create_slip_item(slip)
self.slip_list.controls.append(slip_item)
self.page.update()
except Exception as e:
logging.error(f"伝票読み込みエラー: {e}")
def create_slip_item(self, slip: tuple, is_highlighted: bool = False) -> ft.Container:
"""伝票アイテム作成(ハイライト対応)"""
slip_id, slip_type, customer_name, amount, date, status, description, created_at = slip
# ステータスに応じた色
status_colors = {
"下書き": ft.Colors.ORANGE,
"発行済": ft.Colors.BLUE,
"入金済": ft.Colors.GREEN,
"キャンセル": ft.Colors.RED
}
# タイプに応じたアイコン
type_icons = {
"売上伝票": "💰",
"見積書": "📄",
"納品書": "📦",
"請求書": "📋",
"領収書": "🧾"
}
# ハイライト効果
if is_highlighted:
bgcolor = ft.Colors.BLUE_50
border_color = ft.Colors.BLUE_600
opacity = 1.0
shadow = ft.BoxShadow(
spread_radius=2,
blur_radius=8,
color=ft.Colors.with_opacity(0.3, ft.Colors.BLUE),
offset=ft.Offset(0, 4)
)
else:
bgcolor = ft.Colors.WHITE
border_color = ft.Colors.GREY_200
opacity = 0.3 # グレーアウト
shadow = None
return ft.Container(
content=ft.Row([
# アイコン
ft.Container(
content=ft.Text(type_icons.get(slip_type, "📝"), size=24),
width=50,
height=50,
bgcolor=ft.Colors.BLUE_50 if is_highlighted else ft.Colors.GREY_100,
alignment=ft.alignment.Alignment(0, 0),
border_radius=10
),
# メイン情報
ft.Column([
ft.Text(f"{slip_type} - {customer_name}", size=14, weight=ft.FontWeight.BOLD),
ft.Text(f"¥{amount:,.0f} - {date}", size=12, color=ft.Colors.GREY_600),
ft.Text(description or "", size=10, color=ft.Colors.GREY_500)
], expand=True),
# ステータス
ft.Container(
content=ft.Text(status, size=10, color=ft.Colors.WHITE),
padding=ft.Padding.symmetric(horizontal=8, vertical=4),
bgcolor=status_colors.get(status, ft.Colors.GREY),
border_radius=10
)
], spacing=10),
padding=12,
bgcolor=bgcolor,
border_radius=10,
border=ft.Border.all(2, border_color),
opacity=opacity,
shadow=shadow,
on_click=lambda _: self.open_slip(slip_id)
)
def on_search_change(self, e):
"""検索変更時"""
search_text = e.control.value.lower()
# TODO: 検索機能実装
pass
def filter_by_type(self, type_value: str):
"""タイプでフィルター"""
self.load_slips(type_value)
def open_slip(self, slip_id: int):
"""伝票を開く"""
self.page.snack_bar = ft.SnackBar(
content=ft.Text(f"伝票#{slip_id}を開きます"),
bgcolor=ft.Colors.BLUE
)
self.page.snack_bar.open = True
self.page.update()
# TODO: 伝票詳細画面へ遷移
pass
def main(page: ft.Page):
"""メイン関数"""
try:
explorer = SlipExplorer(page)
logging.info("伝票エクスプローラー起動完了")
except Exception as e:
logging.error(f"伝票エクスプローラー起動エラー: {e}")
page.snack_bar = ft.SnackBar(
content=ft.Text(f"起動エラー: {e}"),
bgcolor=ft.Colors.RED
)
page.snack_bar.open = True
page.update()
if __name__ == "__main__":
ft.run(main)

412
app_slip_interactive.py Normal file
View file

@ -0,0 +1,412 @@
"""
インタラクティブ伝票ビューア
Flutter参考プロジェクトの構造を適用
"""
import flet as ft
import sqlite3
import signal
import sys
import logging
from datetime import datetime
from components.pinch_handler import PinchHandler
from models.invoice_models import DocumentType, Invoice, create_sample_invoices
# カラーテーマ定義
DARK_THEME = {
'background': ft.Colors.GREY_900,
'card_bg': ft.Colors.GREY_800,
'text_primary': ft.Colors.WHITE,
'text_secondary': ft.Colors.GREY_300,
'accent': ft.Colors.BLUE_400
}
LIGHT_THEME = {
'background': ft.Colors.BLUE_50,
'card_bg': ft.Colors.WHITE,
'text_primary': ft.Colors.BLACK,
'text_secondary': ft.Colors.GREY_700,
'accent': ft.Colors.BLUE_600
}
class InteractiveSlipViewer:
def __init__(self, page: ft.Page):
self.page = page
# 状態管理を先に初期化
self.slip_data = []
self.test_mode = False # テストモード
self.test_logs = [] # 操作ログ
self.is_dark_theme = False # テーマ設定
# ページ設定
self.setup_page()
# データベース設定
self.setup_database()
# ピンチハンドラー初期化
self.pinch_handler = PinchHandler(page)
self.pinch_handler.set_callbacks(
on_zoom_change=self.on_zoom_change,
on_tap=self.on_slip_tap,
on_double_tap=self.on_slip_double_tap,
on_long_press=self.on_slip_long_press
)
# UI初期化
self.setup_ui()
def setup_page(self):
self.page.title = "インタラクティブ伝票ビューア"
self.page.window.width = 420
self.page.window.height = 900
self.page.window.resizable = False
self.page.window_center = True
# キーボードイベントハンドラー
self.page.on_keyboard_event = self.on_keyboard_event
def setup_database(self):
try:
self.conn = sqlite3.connect('sales_assist.db')
self.cursor = self.conn.cursor()
# 伝票テーブル作成
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS slips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slip_type TEXT,
customer_name TEXT,
amount REAL,
date TEXT,
status TEXT,
description TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
# サンプルデータ
self.create_sample_data()
except Exception as e:
logging.error(f"DBエラー: {e}")
self.conn = None
def create_sample_data(self):
"""サンプル伝票データ作成"""
try:
# Flutterモデルからサンプルデータを生成
sample_invoices = create_sample_invoices()
for invoice in sample_invoices:
self.cursor.execute('''
INSERT OR REPLACE INTO slips
(slip_type, customer_name, amount, date, status, description)
VALUES (?, ?, ?, ?, ?, ?)
''', (
invoice.document_type.value,
invoice.customer.formal_name,
invoice.total_amount,
invoice.date.strftime('%Y-%m-%d'),
'完了',
invoice.notes
))
self.conn.commit()
except Exception as e:
logging.error(f"サンプルデータ作成エラー: {e}")
def setup_ui(self):
# テーマ設定
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
# ヘッダー
header = ft.Container(
content=ft.Row([
ft.Text("📋 インタラクティブ伝票", size=18, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Container(expand=True),
ft.IconButton(ft.Icons.SEARCH, icon_size=16, icon_color=theme['text_primary']),
ft.Button(
"テスト",
bgcolor=theme['accent'],
color=theme['text_primary'],
on_click=self.toggle_test_mode
)
]),
padding=15,
bgcolor=theme['accent'],
border_radius=15,
margin=ft.Margin.only(bottom=10)
)
# 伝票グリッド
self.slip_grid = ft.GridView(
runs_count=2,
spacing=10,
run_spacing=10,
child_aspect_ratio=1.2,
padding=ft.Padding.symmetric(horizontal=15, vertical=10)
)
# 詳細パネル
self.detail_panel = ft.Container(
content=ft.Text("詳細パネル"),
visible=False,
padding=15,
bgcolor=theme['card_bg'],
border_radius=10,
shadow=ft.BoxShadow(
spread_radius=1,
blur_radius=5,
color=ft.Colors.with_opacity(0.2, ft.Colors.BLACK),
offset=ft.Offset(0, 2)
)
)
# テストモードパネル
self.test_panel = ft.Container(
content=ft.Column([
ft.Text("🧪 テストモード", size=14, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Divider(height=1),
ft.Text("キーボード操作:", size=12, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Text("+ : ズームイン", size=10, color=theme['text_secondary']),
ft.Text("- : ズームアウト", size=10, color=theme['text_secondary']),
ft.Text("R : ズームリセット", size=10, color=theme['text_secondary']),
ft.Text(f"現在のズーム: {int(self.pinch_handler.zoom_level * 100)}%", size=10, color=theme['text_primary']),
ft.Text("操作履歴:", size=12, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Column([], spacing=2)
], spacing=5),
visible=False,
padding=10,
bgcolor=theme['card_bg'],
border_radius=10,
margin=ft.Margin.only(bottom=10)
)
# メインコンテナ
main_container = ft.Column([
self.slip_grid,
self.test_panel,
self.detail_panel
], scroll=ft.ScrollMode.AUTO)
# ページに追加
self.page.add(
ft.Column([
header,
ft.Container(
content=main_container,
expand=True,
bgcolor=theme['background']
)
])
)
# 初期データ読み込み
self.load_slips()
def load_slips(self):
"""伝票データ読み込み"""
if not self.conn:
logging.error("データベース接続がありません")
return
try:
self.cursor.execute("SELECT * FROM slips ORDER BY date DESC")
rows = self.cursor.fetchall()
# タプルからInvoiceオブジェクトに変換
self.slip_data = []
for row in rows:
# 簡単なInvoiceオブジェクトを作成復元用
from models.invoice_models import Customer, InvoiceItem, DocumentType
customer = Customer(1, row[2], row[2]) # id, name, formal_name
items = [InvoiceItem("サンプル明細", 1, int(row[3]))] # description, quantity, unit_price
# DocumentTypeの文字列からEnumに変換
doc_type_str = row[1]
doc_type = next((dt for dt in DocumentType if dt.value == doc_type_str), DocumentType.INVOICE)
invoice = Invoice(
customer=customer,
date=datetime.strptime(row[4], '%Y-%m-%d'),
items=items,
document_type=doc_type
)
self.slip_data.append(invoice)
self.update_slip_grid()
except Exception as e:
logging.error(f"伝票読み込みエラー: {e}")
self.slip_data = []
def update_slip_grid(self):
"""伝票グリッド更新"""
self.slip_grid.controls.clear()
for slip in self.slip_data:
slip_card = self.create_slip_card(slip)
self.slip_grid.controls.append(slip_card)
self.page.update()
def create_slip_card(self, invoice: Invoice) -> ft.Container:
"""伝票カード作成"""
# テーマ取得
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
# ドキュメントタイプに応じたアイコンと色
type_config = {
DocumentType.SALES: {"icon": "💰", "color": ft.Colors.GREEN},
DocumentType.ESTIMATE: {"icon": "📄", "color": ft.Colors.BLUE},
DocumentType.DELIVERY: {"icon": "📦", "color": ft.Colors.PURPLE},
DocumentType.INVOICE: {"icon": "📋", "color": ft.Colors.ORANGE},
DocumentType.RECEIPT: {"icon": "🧾", "color": ft.Colors.RED}
}
config = type_config.get(invoice.document_type, {"icon": "📝", "color": ft.Colors.GREY})
# 通常のカード作成
card = ft.Container(
content=ft.Column([
ft.Container(
content=ft.Text(config["icon"], size=24),
width=40,
height=40,
bgcolor=config["color"],
border_radius=20,
alignment=ft.alignment.Alignment(0, 0)
),
ft.Text(invoice.document_type.value, size=12, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.Text(f"{invoice.customer.formal_name} ¥{invoice.total_amount:,}", size=10, color=theme['text_secondary']),
], spacing=5, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
padding=10,
bgcolor=theme['card_bg'],
border_radius=10,
shadow=ft.BoxShadow(
spread_radius=1,
blur_radius=5,
color=ft.Colors.with_opacity(0.2, ft.Colors.BLACK),
offset=ft.Offset(0, 2)
)
)
return ft.GestureDetector(
content=card,
on_tap=lambda _: self.on_slip_tap(invoice)
)
def on_zoom_change(self, zoom_level: float):
"""ズームレベル変更時"""
# ズーム機能は一時的に無効化
pass
def on_slip_tap(self, invoice: Invoice):
"""伝票タップ"""
self.show_slip_detail(invoice)
def on_slip_double_tap(self, invoice: Invoice):
"""伝票ダブルタップ"""
self.show_slip_detail(invoice)
def on_slip_long_press(self, invoice: Invoice):
"""伝票ロングプレス"""
self.show_slip_detail(invoice)
def show_slip_detail(self, invoice: Invoice):
"""伝票詳細表示"""
# テーマ取得
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
self.detail_panel.content = ft.Column([
ft.Row([
ft.Text("伝票詳細", size=16, weight=ft.FontWeight.BOLD, color=theme['text_primary']),
ft.IconButton(ft.Icons.CLOSE, on_click=self.hide_detail, icon_color=theme['text_primary'])
]),
ft.Divider(),
ft.Text(f"種類: {invoice.document_type.value}", size=14, color=theme['text_primary']),
ft.Text(f"顧客: {invoice.customer.formal_name}", size=14, color=theme['text_primary']),
ft.Text(f"金額: ¥{invoice.total_amount:,}", size=14, color=theme['text_primary']),
ft.Text(f"日付: {invoice.date.strftime('%Y/%m/%d')}", size=14, color=theme['text_primary']),
ft.Text(f"請求書番号: {invoice.invoice_number}", size=14, color=theme['text_primary']),
ft.Text(f"説明: {invoice.notes or 'なし'}", size=12, color=theme['text_secondary']),
ft.Row([
ft.Button("編集", bgcolor=theme['accent'], color=theme['text_primary']),
ft.Button("削除", bgcolor=ft.Colors.RED, color=theme['text_primary']),
], spacing=10)
], spacing=10)
self.detail_panel.visible = True
self.page.update()
def on_keyboard_event(self, e: ft.KeyboardEvent):
"""キーボードイベント処理"""
if self.test_mode:
if e.key == "+":
self.pinch_handler.zoom_in()
self.add_test_log("キーボード: + (ズームイン)")
elif e.key == "-":
self.pinch_handler.zoom_out()
self.add_test_log("キーボード: - (ズームアウト)")
elif e.key == "r":
self.pinch_handler.reset_zoom()
self.add_test_log("キーボード: R (ズームリセット)")
elif e.key == " ": # Spaceキー
self.add_test_log("キーボード: Space (ダブルタップ代替)")
elif e.key == "t": # Tキー
self.toggle_test_mode()
def on_mouse_event(self, e):
"""マウスイベント処理"""
if self.test_mode:
if hasattr(e, 'button'):
if e.button == "right":
self.pinch_handler.zoom_in()
self.add_test_log(f"マウス: 右クリック (ズームイン)")
elif e.button == "middle":
self.pinch_handler.zoom_out()
self.add_test_log(f"マウス: 中クリック (ズームアウト)")
def toggle_test_mode(self):
"""テストモード切替"""
self.test_mode = not self.test_mode
self.test_panel.visible = self.test_mode
self.page.update()
def add_test_log(self, message: str):
"""テストログ追加"""
if hasattr(self, 'test_logs'):
self.test_logs.append(message)
else:
self.test_logs = [message]
# ログ表示更新
if len(self.test_logs) > 10: # 最新10件のみ表示
self.test_logs = self.test_logs[-10:]
# テーマ取得
theme = DARK_THEME if self.is_dark_theme else LIGHT_THEME
log_container = ft.Column([
ft.Text(log, size=8, color=theme['text_secondary'])
for log in self.test_logs
])
self.test_panel.content.controls[-1].controls = [log_container]
self.page.update()
def hide_detail(self, e=None):
"""詳細パネル非表示"""
self.detail_panel.visible = False
self.page.update()
def main(page: ft.Page):
try:
viewer = InteractiveSlipViewer(page)
logging.info("インタラクティブ伝票ビューア起動")
except Exception as e:
logging.error(f"起動エラー: {e}")
if __name__ == "__main__":
ft.run(main)

View file

@ -0,0 +1,158 @@
"""
顧客選択モーダルコンポーネント
Flutter風のModalBottomSheetをFletで実装
"""
import flet as ft
from typing import List, Callable, Optional
from models.invoice_models import Customer
class CustomerPickerModal:
"""顧客選択モーダル"""
def __init__(self, page: ft.Page, customers: List[Customer],
on_customer_selected: Callable[[Customer], None],
on_customer_deleted: Callable[[Customer], None] = None):
self.page = page
self.customers = customers
self.on_customer_selected = on_customer_selected
self.on_customer_deleted = on_customer_deleted
self.is_open = False
def open(self):
"""モーダルを開く"""
self.is_open = True
self.show_modal()
def close(self):
"""モーダルを閉じる"""
self.is_open = False
self.hide_modal()
def show_modal(self):
"""モーダル表示"""
# 検索テキストフィールド
search_field = ft.TextField(
label="顧客検索",
prefix_icon=ft.Icons.SEARCH,
on_change=self.filter_customers,
expand=True,
)
# 顧客リスト
customer_list = ft.Column([], scroll=ft.ScrollMode.AUTO, expand=True)
# モーダルダイアログ
modal_dialog = ft.AlertDialog(
title=ft.Text("顧客選択"),
content=ft.Column([
search_field,
ft.Container(height=10),
ft.Container(
content=customer_list,
height=300,
width=400,
),
ft.Container(height=10),
ft.Button(
content=ft.Row([
ft.Icon(ft.Icons.ADD),
ft.Text("新規顧客を登録"),
], alignment=ft.MainAxisAlignment.CENTER),
on_click=self.add_new_customer,
bgcolor=ft.Colors.BLUE_GREY_800,
),
], tight=True),
actions=[
ft.TextButton("キャンセル", on_click=lambda _: self.close()),
],
actions_alignment=ft.MainAxisAlignment.END,
)
# ダイアログを開く
self.page.dialog = modal_dialog
modal_dialog.open = True
self.page.update()
# 初期データ表示
self.update_customer_list_simple(customer_list, self.customers)
def hide_modal(self):
"""モーダルを非表示"""
self.page.overlay.clear()
self.page.update()
def filter_customers(self, e=None):
"""顧客フィルタリング"""
search_text = e.control.value.lower() if e else ""
filtered_customers = [
customer for customer in self.customers
if search_text in customer.name.lower() or
search_text in customer.formal_name.lower()
]
self.update_customer_list(filtered_customers)
def update_customer_list_simple(self, customer_list, customers: List[Customer]):
"""顧客リスト更新(シンプル版)"""
customer_list.controls.clear()
for customer in customers:
card = self.create_customer_card(customer)
customer_list.controls.append(card)
self.page.update()
def update_customer_list(self, customers: List[Customer]):
"""顧客リスト更新"""
# ダイアログの顧客リストを更新
if hasattr(self.page, 'dialog') and self.page.dialog:
customer_list = self.page.dialog.content.controls[2].content
self.update_customer_list_simple(customer_list, customers)
def create_customer_card(self, customer: Customer) -> ft.Container:
"""顧客カード作成"""
return ft.Container(
content=ft.Card(
content=ft.Container(
content=ft.Column([
ft.Row([
ft.Text(
customer.formal_name,
size=16,
weight=ft.FontWeight.BOLD,
expand=True,
),
ft.IconButton(
ft.Icons.DELETE,
on_click=lambda _, c=customer: self.delete_customer(c),
icon_color=ft.Colors.RED_400,
),
]),
ft.Container(height=5),
ft.Text(customer.address, size=12, color=ft.Colors.GREY_600),
ft.Text(customer.phone, size=12, color=ft.Colors.GREY_600),
]),
padding=ft.padding.all(15),
),
elevation=2,
),
on_click=lambda _, c=customer: self.select_customer(c),
)
def select_customer(self, customer: Customer):
"""顧客選択"""
if self.on_customer_selected:
self.on_customer_selected(customer)
self.close()
def delete_customer(self, customer: Customer):
"""顧客削除"""
if self.on_customer_deleted:
self.on_customer_deleted(customer)
def add_new_customer(self, e=None):
"""新規顧客追加"""
# TODO: 新規顧客登録画面を開く
logging.info("新規顧客登録")

180
components/pinch_handler.py Normal file
View file

@ -0,0 +1,180 @@
"""
ピンチ操作ハンドラー
アプリ全体でピンチインズーム機能を提供する共通コンポーネント
"""
import flet as ft
from typing import Optional, Callable
class PinchHandler:
"""ピンチ操作ハンドラー"""
def __init__(self, page: ft.Page):
self.page = page
self.zoom_level = 1.0
self.min_zoom = 0.6
self.max_zoom = 2.0
self.zoom_step = 0.2
# コールバック関数
self.on_zoom_change: Optional[Callable[[float], None]] = None
self.on_tap: Optional[Callable] = None
self.on_double_tap: Optional[Callable] = None
self.on_long_press: Optional[Callable] = None
# 状態管理
self.last_distance = 0
self.is_zooming = False
def set_callbacks(self,
on_zoom_change: Optional[Callable[[float], None]] = None,
on_tap: Optional[Callable] = None,
on_double_tap: Optional[Callable] = None,
on_long_press: Optional[Callable] = None):
"""コールバック関数設定"""
self.on_zoom_change = on_zoom_change
self.on_tap = on_tap
self.on_double_tap = on_double_tap
self.on_long_press = on_long_press
def create_gesture_detector(self, content: ft.Control, on_click: Optional[Callable] = None) -> ft.GestureDetector:
"""ジェスチャー検出付きコンテナ作成"""
return ft.GestureDetector(
content=content,
on_tap=on_click
)
def _handle_tap(self, e):
"""タップ処理"""
if self.on_tap:
self.on_tap(e)
def _handle_double_tap(self, e):
"""ダブルタップ処理"""
self.zoom_in()
if self.on_double_tap:
self.on_double_tap(e)
def _handle_long_press(self, e):
"""長押し処理"""
if self.on_long_press:
self.on_long_press(e)
def _handle_tap_update(self, e):
"""タップ位置更新(ピンチ検出用)"""
# TODO: 実際のピンチ検出ロジック
# Fletの制限により、現在はボタンでのズームをメインに
pass
def zoom_in(self):
"""ズームイン"""
if self.zoom_level < self.max_zoom:
self.zoom_level += self.zoom_step
self._notify_zoom_change()
def zoom_out(self):
"""ズームアウト"""
if self.zoom_level > self.min_zoom:
self.zoom_level -= self.zoom_step
self._notify_zoom_change()
def set_zoom(self, level: float):
"""ズームレベル設定"""
self.zoom_level = max(self.min_zoom, min(self.max_zoom, level))
self._notify_zoom_change()
def reset_zoom(self):
"""ズームリセット"""
self.zoom_level = 1.0
self._notify_zoom_change()
def _notify_zoom_change(self):
"""ズーム変更通知"""
if self.on_zoom_change:
self.on_zoom_change(self.zoom_level)
def get_zoom_controls(self) -> ft.Row:
"""ズームコントロールUI作成"""
return ft.Row([
ft.IconButton(
ft.Icons.ZOOM_OUT,
icon_size=20,
tooltip="縮小",
on_click=lambda _: self.zoom_out()
),
ft.Text(f"{int(self.zoom_level * 100)}%", size=12),
ft.IconButton(
ft.Icons.ZOOM_IN,
icon_size=20,
tooltip="拡大",
on_click=lambda _: self.zoom_in()
),
ft.IconButton(
ft.Icons.REFRESH,
icon_size=20,
tooltip="リセット",
on_click=lambda _: self.reset_zoom()
)
], spacing=5)
def apply_zoom_to_size(self, base_size: float) -> float:
"""ズームをサイズに適用"""
return base_size * self.zoom_level
def apply_zoom_to_text_size(self, base_size: float) -> float:
"""ズームをテキストサイズに適用"""
return base_size * self.zoom_level
class ZoomableContainer:
"""ズーム対応コンテナ"""
def __init__(self, pinch_handler: PinchHandler):
self.pinch_handler = pinch_handler
self.base_width = 100
self.base_height = 100
self.base_text_size = 12
self.base_padding = 10
def create_zoomable_card(self,
icon: str,
title: str,
subtitle: str,
color: ft.Colors,
on_click: Optional[Callable] = None) -> ft.Container:
"""ズーム対応カード作成"""
# ズーム適用
width = self.pinch_handler.apply_zoom_to_size(self.base_width)
height = self.pinch_handler.apply_zoom_to_size(self.base_height)
text_size = self.pinch_handler.apply_zoom_to_text_size(self.base_text_size)
padding = self.pinch_handler.apply_zoom_to_size(self.base_padding)
card = ft.Container(
content=ft.Column([
ft.Container(
content=ft.Text(icon, size=text_size * 2),
width=width * 0.5,
height=height * 0.5,
bgcolor=color,
alignment=ft.alignment.Alignment(0, 0),
border_radius=padding
),
ft.Text(title, size=text_size, weight=ft.FontWeight.BOLD),
ft.Text(subtitle, size=text_size * 0.8, color=ft.Colors.GREY_600)
], spacing=5),
width=width,
height=height,
padding=padding,
bgcolor=ft.Colors.WHITE,
border_radius=padding,
shadow=ft.BoxShadow(
spread_radius=1,
blur_radius=5,
color=ft.Colors.with_opacity(0.2, ft.Colors.GREY),
offset=ft.Offset(0, 2)
),
on_click=on_click
)
return self.pinch_handler.create_gesture_detector(card)

58
flutter.参考/.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# General
*.log
.DS_Store
# system-specific
*~
*.swp
# IDE configurations
.idea/
.vscode/
# Flutter/Dart specific
.dart_tool/
.flutter-plugins-dependencies
# ios/Pods/ is often ignored, but sometimes specific projects include it
# ios/Pods/
# For macOS desktop builds
macos/Runner/Flutter/AppFrameworkInfo.plist
macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
# For Windows desktop builds
windows/flutter/ephemeral/
# Android specific
android/.gradle/
android/gradle/wrapper/gradle-wrapper.properties
android/app/build.gradle.kts # Usually generated, but can be ignored if specific configurations are managed elsewhere or to prevent accidental commits
android/app/build/ # Build artifacts
android/captures/ # For Android Studio captures
android/gradle.properties # Usually fine to commit, but depends on project setup
# iOS specific
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json # Example for specific asset files, usually not ignored unless generated
ios/Runner.xcworkspace/contents.xcworkspacedata
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
# Web specific
web/icons/
web/manifest.json
# Build output
build/
# Dependency caching
.pub-cache/
# OS-generated files
.DS_Store
Thumbs.db
# IDE settings (IntelliJ IDEA)
*.iml
# Temporary files
*.tmp

45
flutter.参考/.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "67323de285b00232883f53b84095eb72be97d35c"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: android
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: ios
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: linux
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: macos
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: web
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: windows
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
flutter.参考/README.md Normal file
View file

@ -0,0 +1,16 @@
# gemi_invoice
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,9 @@
既存の lib/ 内のモデルを確認し、請求書Invoiceと領収書ReceiptのPDF出力機能を実装せよ。
printing パッケージを使用して、プレビュー画面とPDF保存機能を実装すること。
レイアウトは日本の商習慣に合わせた標準的なものとし、ロゴ、会社名、インボイス登録番号、明細、合計金額、登録日を表示すること。
重要: PDFを生成する際、将来のOdoo連携のために、ファイル名には {会社ID}_{端末ID}_{連番}.pdf という命名規則を適用せよ。
生成したPDFのバイナリを share_plus で外部メールやLINEに共有できるボタンをUIに追加せよ。

View file

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
flutter.参考/android/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View file

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.gemi_invoice"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.gemi_invoice"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,56 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.gemi_invoice"
>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="file" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="content" />
</intent>
<intent>
<action android:name="android.intent.action.PICK" />
<data android:mimeType="vnd.android.cursor.dir/contact" />
</intent>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
<application
android:label="gemi_invoice"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
>
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="flutterEmbedding" android:value="2" />
</application>
</manifest>

View file

@ -0,0 +1,5 @@
package com.example.gemi_invoice
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View file

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

Binary file not shown.

View file

@ -0,0 +1,41 @@
# collection_flutter_code.py
# Version: 1.0.0 (2025-07-04)
# Description: Flutterのlib配下のコードを集約し、AIへの受け渡しを最適化する
import os
def collect_flutter_code(target_dir='lib', output_file='flutter_bundle_for_ai.txt'):
# AIが識別しやすいようにヘッダーを付与
header = """
# ==========================================
# FLUTTER CODE BUNDLE FOR AI ANALYSIS
# PROJECT: Flutter to Kivy Migration
# ==========================================
"""
collected_data = [header]
if not os.path.exists(target_dir):
print(f"Error: {target_dir} ディレクトリが見つかりません。")
return
for root, dirs, files in os.walk(target_dir):
for file in files:
if file.endswith('.dart'):
file_path = os.path.join(root, file)
relative_path = os.path.relpath(file_path, target_dir)
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# AIにファイル構造を理解させるためのデリミタ
collected_data.append(f"\n\n--- FILE: {relative_path} ---")
collected_data.append(content)
with open(output_file, 'w', encoding='utf-8') as f:
f.write("\n".join(collected_data))
print(f"成功: {output_file} に全コードを回収しました。")
print("このファイルの内容をコピーして、私に貼り付けてください。")
if __name__ == "__main__":
collect_flutter_code()

1005
flutter.参考/dir.txt Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

34
flutter.参考/ios/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gemi Invoice</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>gemi_invoice</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View file

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View file

@ -0,0 +1,99 @@
import '../models/invoice_models.dart';
///
/// Odoo IDodooId
class Product {
final String id; // ID
final int? odooId; // Odoo上の product.product ID (nullの場合は未同期)
final String name; //
final int defaultUnitPrice; //
final String? category; //
const Product({
required this.id,
this.odooId,
required this.name,
required this.defaultUnitPrice,
this.category,
});
/// InvoiceItem
InvoiceItem toInvoiceItem({int quantity = 1}) {
return InvoiceItem(
description: name,
quantity: quantity,
unitPrice: defaultUnitPrice,
);
}
///
Product copyWith({
String? id,
int? odooId,
String? name,
int? defaultUnitPrice,
String? category,
}) {
return Product(
id: id ?? this.id,
odooId: odooId ?? this.odooId,
name: name ?? this.name,
defaultUnitPrice: defaultUnitPrice ?? this.defaultUnitPrice,
category: category ?? this.category,
);
}
/// JSON変換 (Odoo同期用)
Map<String, dynamic> toJson() {
return {
'id': id,
'odoo_id': odooId,
'name': name,
'default_unit_price': defaultUnitPrice,
'category': category,
};
}
/// JSONからモデルを生成
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
odooId: json['odoo_id'],
name: json['name'],
defaultUnitPrice: json['default_unit_price'],
category: json['category'],
);
}
}
///
class ProductMaster {
static const List<Product> products = [
Product(id: 'S001', name: 'システム開発費', defaultUnitPrice: 500000, category: '開発'),
Product(id: 'S002', name: '保守・メンテナンス費', defaultUnitPrice: 50000, category: '運用'),
Product(id: 'S003', name: '技術コンサルティング', defaultUnitPrice: 100000, category: '開発'),
Product(id: 'G001', name: 'ライセンス料 (Pro)', defaultUnitPrice: 15000, category: '製品'),
Product(id: 'G002', name: '初期導入セットアップ', defaultUnitPrice: 30000, category: '製品'),
Product(id: 'M001', name: 'ハードウェア一式', defaultUnitPrice: 250000, category: '物品'),
Product(id: 'Z001', name: '諸経費', defaultUnitPrice: 5000, category: 'その他'),
];
///
static List<String> get categories {
return products.map((p) => p.category ?? 'その他').toSet().toList();
}
///
static List<Product> getProductsByCategory(String category) {
return products.where((p) => (p.category ?? 'その他') == category).toList();
}
/// IDで検索
static List<Product> search(String query) {
final q = query.toLowerCase();
return products.where((p) =>
p.name.toLowerCase().contains(q) ||
p.id.toLowerCase().contains(q)
).toList();
}
}

View file

@ -0,0 +1,145 @@
// lib/main.dart
// version: 1.4.3c (Bug Fix: PDF layout error) - Refactored for modularity and history management
import 'package:flutter/material.dart';
// --- ---
import 'models/invoice_models.dart';
import 'screens/invoice_input_screen.dart';
import 'screens/invoice_detail_page.dart';
import 'screens/invoice_history_screen.dart';
import 'screens/company_editor_screen.dart'; //
void main() {
runApp(const MyApp());
}
//
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '販売アシスト1号',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
fontFamily: 'IPAexGothic',
),
home: const MainNavigationShell(),
);
}
}
///
class MainNavigationShell extends StatefulWidget {
const MainNavigationShell({super.key});
@override
State<MainNavigationShell> createState() => _MainNavigationShellState();
}
class _MainNavigationShellState extends State<MainNavigationShell> {
int _selectedIndex = 0;
//
final List<Widget> _screens = [];
@override
void initState() {
super.initState();
_screens.addAll([
InvoiceFlowScreen(onMoveToHistory: () => _onItemTapped(1)),
const InvoiceHistoryScreen(),
]);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
//
void _openCompanyEditor(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompanyEditorScreen(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _screens,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.add_box),
label: '新規作成',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: '発行履歴',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.indigo,
onTap: _onItemTapped,
),
);
}
}
///
class InvoiceFlowScreen extends StatelessWidget {
final VoidCallback onMoveToHistory;
const InvoiceFlowScreen({super.key, required this.onMoveToHistory});
// PDF
void _handleInvoiceGenerated(BuildContext context, Invoice generatedInvoice, String filePath) {
// PDF生成DB保存後に詳細ページへ遷移
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(invoice: generatedInvoice),
),
);
}
//
void _openCompanyEditor(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompanyEditorScreen(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//
title: GestureDetector(
onLongPress: () => _openCompanyEditor(context),
child: const Text("販売アシスト1号 V1.4.3c"),
),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
),
//
body: InvoiceInputForm(
onInvoiceGenerated: (invoice, path) => _handleInvoiceGenerated(context, invoice, path),
),
);
}
}

View file

@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

View file

@ -0,0 +1,106 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
///
///
class Company {
final String id; // ID ()
final String formalName; // (: )
final String? representative; //
final String? zipCode; // 便
final String? address; //
final String? tel; //
final String? fax; // FAX番号
final String? email; //
final String? website; //
final String? registrationNumber; // ()
final String? notes; //
const Company({
required this.id,
required this.formalName,
this.representative,
this.zipCode,
this.address,
this.tel,
this.fax,
this.email,
this.website,
this.registrationNumber,
this.notes,
});
///
Company copyWith({
String? id,
String? formalName,
String? representative,
String? zipCode,
String? address,
String? tel,
String? fax,
String? email,
String? website,
String? registrationNumber,
String? notes,
}) {
return Company(
id: id ?? this.id,
formalName: formalName ?? this.formalName,
representative: representative ?? this.representative,
zipCode: zipCode ?? this.zipCode,
address: address ?? this.address,
tel: tel ?? this.tel,
fax: fax ?? this.fax,
email: email ?? this.email,
website: website ?? this.website,
registrationNumber: registrationNumber ?? this.registrationNumber,
notes: notes ?? this.notes,
);
}
/// JSON変換 ()
Map<String, dynamic> toJson() {
return {
'id': id,
'formal_name': formalName,
'representative': representative,
'zip_code': zipCode,
'address': address,
'tel': tel,
'fax': fax,
'email': email,
'website': website,
'registration_number': registrationNumber,
'notes': notes,
};
}
/// JSONからモデルを生成
factory Company.fromJson(Map<String, dynamic> json) {
return Company(
id: json['id'] as String,
formalName: json['formal_name'] as String,
representative: json['representative'] as String?,
zipCode: json['zip_code'] as String?,
address: json['address'] as String?,
tel: json['tel'] as String?,
fax: json['fax'] as String?,
email: json['email'] as String?,
website: json['website'] as String?,
registrationNumber: json['registration_number'] as String?,
notes: json['notes'] as String?,
);
}
// ()
static const Company defaultCompany = Company(
id: 'my_company',
formalName: '自社名が入ります',
zipCode: '〒000-0000',
address: '住所がここに入ります',
tel: 'TEL: 00-0000-0000',
registrationNumber: '適格請求書発行事業者登録番号 T1234567890123', //
notes: 'いつもお世話になっております。',
);
}

View file

@ -0,0 +1,87 @@
import 'package:intl/intl.dart';
///
/// Odoo IDodooId
class Customer {
final String id; // ID
final int? odooId; // Odoo上の res.partner ID (nullの場合は未同期)
final String displayName; //
final String formalName; //
final String? zipCode; // 便
final String? address; //
final String? department; //
final String? title; // ()
final DateTime lastUpdatedAt; //
Customer({
required this.id,
this.odooId,
required this.displayName,
required this.formalName,
this.zipCode,
this.address,
this.department,
this.title = '御中',
DateTime? lastUpdatedAt,
}) : this.lastUpdatedAt = lastUpdatedAt ?? DateTime.now();
///
String get invoiceName => department != null && department!.isNotEmpty
? "$formalName\n$department $title"
: "$formalName $title";
///
Customer copyWith({
String? id,
int? odooId,
String? displayName,
String? formalName,
String? zipCode,
String? address,
String? department,
String? title,
DateTime? lastUpdatedAt,
}) {
return Customer(
id: id ?? this.id,
odooId: odooId ?? this.odooId,
displayName: displayName ?? this.displayName,
formalName: formalName ?? this.formalName,
zipCode: zipCode ?? this.zipCode,
address: address ?? this.address,
department: department ?? this.department,
title: title ?? this.title,
lastUpdatedAt: lastUpdatedAt ?? DateTime.now(),
);
}
/// JSON変換 (Odoo同期用)
Map<String, dynamic> toJson() {
return {
'id': id,
'odoo_id': odooId,
'display_name': displayName,
'formal_name': formalName,
'zip_code': zipCode,
'address': address,
'department': department,
'title': title,
'last_updated_at': lastUpdatedAt.toIso8601String(),
};
}
/// JSONからモデルを生成
factory Customer.fromJson(Map<String, dynamic> json) {
return Customer(
id: json['id'],
odooId: json['odoo_id'],
displayName: json['display_name'],
formalName: json['formal_name'],
zipCode: json['zip_code'],
address: json['address'],
department: json['department'],
title: json['title'] ?? '御中',
lastUpdatedAt: DateTime.parse(json['last_updated_at']),
);
}
}

View file

@ -0,0 +1,180 @@
// lib/models/invoice_models.dart
import 'package:intl/intl.dart';
import 'customer_model.dart';
///
enum DocumentType {
estimate('見積書'),
delivery('納品書'),
invoice('請求書'),
receipt('領収書');
final String label;
const DocumentType(this.label);
}
///
class InvoiceItem {
String description;
int quantity;
int unitPrice;
bool isDiscount; //
InvoiceItem({
required this.description,
required this.quantity,
required this.unitPrice,
this.isDiscount = false, // false ()
});
// ( * )
int get subtotal => quantity * unitPrice * (isDiscount ? -1 : 1);
//
InvoiceItem copyWith({
String? description,
int? quantity,
int? unitPrice,
bool? isDiscount,
}) {
return InvoiceItem(
description: description ?? this.description,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
isDiscount: isDiscount ?? this.isDiscount,
);
}
// JSON変換
Map<String, dynamic> toJson() {
return {
'description': description,
'quantity': quantity,
'unit_price': unitPrice,
'is_discount': isDiscount,
};
}
// JSONから復元
factory InvoiceItem.fromJson(Map<String, dynamic> json) {
return InvoiceItem(
description: json['description'] as String,
quantity: json['quantity'] as int,
unitPrice: json['unit_price'] as int,
isDiscount: json['is_discount'] ?? false,
);
}
}
/// ()
class Invoice {
Customer customer; //
DateTime date;
List<InvoiceItem> items;
String? filePath; // PDFのパス
String invoiceNumber; //
String? notes; //
bool isShared; //
DocumentType type; //
Invoice({
required this.customer,
required this.date,
required this.items,
this.filePath,
String? invoiceNumber,
this.notes,
this.isShared = false,
this.type = DocumentType.invoice,
}) : invoiceNumber = invoiceNumber ?? DateFormat('yyyyMMdd-HHmm').format(date);
//
String get clientName => customer.formalName;
//
int get subtotal {
return items.fold(0, (sum, item) => sum + item.subtotal);
}
// (10%)
int get tax {
return (subtotal * 0.1).floor();
}
//
int get totalAmount {
return subtotal + tax;
}
//
Invoice copyWith({
Customer? customer,
DateTime? date,
List<InvoiceItem>? items,
String? filePath,
String? invoiceNumber,
String? notes,
bool? isShared,
DocumentType? type,
}) {
return Invoice(
customer: customer ?? this.customer,
date: date ?? this.date,
items: items ?? this.items,
filePath: filePath ?? this.filePath,
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
notes: notes ?? this.notes,
isShared: isShared ?? this.isShared,
type: type ?? this.type,
);
}
// CSV形式への変換
String toCsv() {
StringBuffer sb = StringBuffer();
sb.writeln("Type,${type.label}");
sb.writeln("Customer,${customer.formalName}");
sb.writeln("Number,$invoiceNumber");
sb.writeln("Date,${DateFormat('yyyy/MM/dd').format(date)}");
sb.writeln("Shared,${isShared ? 'Yes' : 'No'}");
sb.writeln("");
sb.writeln("Description,Quantity,UnitPrice,Subtotal,IsDiscount"); // isDiscountを追加
for (var item in items) {
sb.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal},${item.isDiscount ? 'Yes' : 'No'}");
}
return sb.toString();
}
// JSON変換 ()
Map<String, dynamic> toJson() {
return {
'customer': customer.toJson(),
'date': date.toIso8601String(),
'items': items.map((item) => item.toJson()).toList(),
'file_path': filePath,
'invoice_number': invoiceNumber,
'notes': notes,
'is_shared': isShared,
'type': type.name, // Enumの名前で保存
};
}
// JSONから復元 ()
factory Invoice.fromJson(Map<String, dynamic> json) {
return Invoice(
customer: Customer.fromJson(json['customer'] as Map<String, dynamic>),
date: DateTime.parse(json['date'] as String),
items: (json['items'] as List)
.map((i) => InvoiceItem.fromJson(i as Map<String, dynamic>))
.toList(),
filePath: json['file_path'] as String?,
invoiceNumber: json['invoice_number'] as String,
notes: (json['notes'] == 'null') ? null : json['notes'] as String?, // 'null'
isShared: json['is_shared'] ?? false,
type: DocumentType.values.firstWhere(
(e) => e.name == (json['type'] ?? 'invoice'),
orElse: () => DocumentType.invoice,
),
);
}
}

View file

@ -0,0 +1,206 @@
// lib/screens/company_editor_screen.dart
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/company_model.dart';
import '../services/master_repository.dart';
///
class CompanyEditorScreen extends StatefulWidget {
const CompanyEditorScreen({super.key});
@override
State<CompanyEditorScreen> createState() => _CompanyEditorScreenState();
}
class _CompanyEditorScreenState extends State<CompanyEditorScreen> {
final _repository = MasterRepository();
final _formKey = GlobalKey<FormState>(); //
late Company _company;
late TextEditingController _formalNameController;
late TextEditingController _representativeController;
late TextEditingController _zipCodeController;
late TextEditingController _addressController;
late TextEditingController _telController;
late TextEditingController _faxController;
late TextEditingController _emailController;
late TextEditingController _websiteController;
late TextEditingController _registrationNumberController;
late TextEditingController _notesController;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCompanyInfo();
}
Future<void> _loadCompanyInfo() async {
setState(() => _isLoading = true);
_company = await _repository.loadCompany();
_formalNameController = TextEditingController(text: _company.formalName);
_representativeController = TextEditingController(text: _company.representative);
_zipCodeController = TextEditingController(text: _company.zipCode);
_addressController = TextEditingController(text: _company.address);
_telController = TextEditingController(text: _company.tel);
_faxController = TextEditingController(text: _company.fax);
_emailController = TextEditingController(text: _company.email);
_websiteController = TextEditingController(text: _company.website);
_registrationNumberController = TextEditingController(text: _company.registrationNumber);
_notesController = TextEditingController(text: _company.notes);
setState(() => _isLoading = false);
}
Future<void> _saveCompanyInfo() async {
if (!_formKey.currentState!.validate()) {
return;
}
final updatedCompany = _company.copyWith(
formalName: _formalNameController.text.trim(),
representative: _representativeController.text.trim(),
zipCode: _zipCodeController.text.trim(),
address: _addressController.text.trim(),
tel: _telController.text.trim(),
fax: _faxController.text.trim(),
email: _emailController.text.trim(),
website: _websiteController.text.trim(),
registrationNumber: _registrationNumberController.text.trim(),
notes: _notesController.text.trim(),
);
await _repository.saveCompany(updatedCompany);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('自社情報を保存しました。')),
);
Navigator.pop(context); //
}
}
@override
void dispose() {
_formalNameController.dispose();
_representativeController.dispose();
_zipCodeController.dispose();
_addressController.dispose();
_telController.dispose();
_faxController.dispose();
_emailController.dispose();
_websiteController.dispose();
_registrationNumberController.dispose();
_notesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("自社情報編集"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveCompanyInfo,
tooltip: "保存",
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: _formalNameController,
decoration: const InputDecoration(labelText: "正式名称 (必須)", border: OutlineInputBorder()),
validator: (value) {
if (value == null || value.isEmpty) {
return '正式名称は必須です';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _representativeController,
decoration: const InputDecoration(labelText: "代表者名", border: OutlineInputBorder()),
),
const SizedBox(height: 16),
TextFormField(
controller: _zipCodeController,
decoration: const InputDecoration(labelText: "郵便番号", border: OutlineInputBorder()),
keyboardType: TextInputType.text,
),
const SizedBox(height: 16),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(labelText: "住所", border: OutlineInputBorder()),
maxLines: 2,
),
const SizedBox(height: 16),
TextFormField(
controller: _telController,
decoration: const InputDecoration(labelText: "電話番号", border: OutlineInputBorder()),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
TextFormField(
controller: _faxController,
decoration: const InputDecoration(labelText: "FAX番号", border: OutlineInputBorder()),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: "メールアドレス", border: OutlineInputBorder()),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 16),
TextFormField(
controller: _websiteController,
decoration: const InputDecoration(labelText: "ウェブサイト", border: OutlineInputBorder()),
keyboardType: TextInputType.url,
),
const SizedBox(height: 16),
TextFormField(
controller: _registrationNumberController,
decoration: const InputDecoration(labelText: "登録番号 (インボイス制度対応)", border: OutlineInputBorder()),
),
const SizedBox(height: 16),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
maxLines: 3,
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _saveCompanyInfo,
icon: const Icon(Icons.save),
label: const Text("自社情報を保存"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,308 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:uuid/uuid.dart';
import '../models/customer_model.dart';
///
class CustomerPickerModal extends StatefulWidget {
final List<Customer> existingCustomers;
final Function(Customer) onCustomerSelected;
final Function(Customer)? onCustomerDeleted; //
const CustomerPickerModal({
Key? key,
required this.existingCustomers,
required this.onCustomerSelected,
this.onCustomerDeleted,
}) : super(key: key);
@override
State<CustomerPickerModal> createState() => _CustomerPickerModalState();
}
class _CustomerPickerModalState extends State<CustomerPickerModal> {
String _searchQuery = "";
List<Customer> _filteredCustomers = [];
bool _isImportingFromContacts = false;
@override
void initState() {
super.initState();
_filteredCustomers = widget.existingCustomers;
}
void _filterCustomers(String query) {
setState(() {
_searchQuery = query.toLowerCase();
_filteredCustomers = widget.existingCustomers.where((customer) {
return customer.formalName.toLowerCase().contains(_searchQuery) ||
customer.displayName.toLowerCase().contains(_searchQuery);
}).toList();
});
}
///
Future<void> _importFromPhoneContacts() async {
setState(() => _isImportingFromContacts = true);
try {
if (await FlutterContacts.requestPermission(readonly: true)) {
final contacts = await FlutterContacts.getContacts();
if (!mounted) return;
setState(() => _isImportingFromContacts = false);
final Contact? selectedContact = await showModalBottomSheet<Contact>(
context: context,
isScrollControlled: true,
builder: (context) => _PhoneContactListSelector(contacts: contacts),
);
if (selectedContact != null) {
_showCustomerEditDialog(
displayName: selectedContact.displayName,
initialFormalName: selectedContact.displayName,
);
}
}
} catch (e) {
setState(() => _isImportingFromContacts = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("電話帳の取得に失敗しました: $e")),
);
}
}
///
void _showCustomerEditDialog({
required String displayName,
required String initialFormalName,
Customer? existingCustomer,
}) {
final formalNameController = TextEditingController(text: initialFormalName);
final departmentController = TextEditingController(text: existingCustomer?.department ?? "");
final addressController = TextEditingController(text: existingCustomer?.address ?? "");
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(existingCustomer == null ? "顧客の新規登録" : "顧客情報の編集"),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("電話帳名: $displayName", style: const TextStyle(fontSize: 12, color: Colors.grey)),
const SizedBox(height: 16),
TextField(
controller: formalNameController,
decoration: const InputDecoration(
labelText: "請求書用 正式名称",
hintText: "株式会社 など",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: departmentController,
decoration: const InputDecoration(
labelText: "部署名",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: addressController,
decoration: const InputDecoration(
labelText: "住所",
border: OutlineInputBorder(),
),
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
ElevatedButton(
onPressed: () {
final updatedCustomer = existingCustomer?.copyWith(
formalName: formalNameController.text.trim(),
department: departmentController.text.trim(),
address: addressController.text.trim(),
) ??
Customer(
id: const Uuid().v4(),
displayName: displayName,
formalName: formalNameController.text.trim(),
department: departmentController.text.trim(),
address: addressController.text.trim(),
);
Navigator.pop(context);
widget.onCustomerSelected(updatedCustomer);
},
child: const Text("保存して確定"),
),
],
),
);
}
///
void _confirmDelete(Customer customer) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("顧客の削除"),
content: Text("${customer.formalName}」をマスターから削除しますか?\n(過去の請求書ファイルは削除されません)"),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton(
onPressed: () {
Navigator.pop(context);
if (widget.onCustomerDeleted != null) {
widget.onCustomerDeleted!(customer);
setState(() {
_filterCustomers(_searchQuery); //
});
}
},
child: const Text("削除する", style: TextStyle(color: Colors.red)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Material(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("顧客マスター管理", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
],
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
hintText: "登録済み顧客を検索...",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
onChanged: _filterCustomers,
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _isImportingFromContacts ? null : _importFromPhoneContacts,
icon: _isImportingFromContacts
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.contact_phone),
label: const Text("電話帳から新規取り込み"),
style: ElevatedButton.styleFrom(backgroundColor: Colors.blueGrey.shade700, foregroundColor: Colors.white),
),
),
],
),
),
const Divider(),
Expanded(
child: _filteredCustomers.isEmpty
? const Center(child: Text("該当する顧客がいません"))
: ListView.builder(
itemCount: _filteredCustomers.length,
itemBuilder: (context, index) {
final customer = _filteredCustomers[index];
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.business)),
title: Text(customer.formalName),
subtitle: Text(customer.department?.isNotEmpty == true ? customer.department! : "部署未設定"),
onTap: () => widget.onCustomerSelected(customer),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blueGrey, size: 20),
onPressed: () => _showCustomerEditDialog(
displayName: customer.displayName,
initialFormalName: customer.formalName,
existingCustomer: customer,
),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.redAccent, size: 20),
onPressed: () => _confirmDelete(customer),
),
],
),
);
},
),
),
],
),
);
}
}
///
class _PhoneContactListSelector extends StatefulWidget {
final List<Contact> contacts;
const _PhoneContactListSelector({required this.contacts});
@override
State<_PhoneContactListSelector> createState() => _PhoneContactListSelectorState();
}
class _PhoneContactListSelectorState extends State<_PhoneContactListSelector> {
List<Contact> _filtered = [];
final _searchController = TextEditingController();
@override
void initState() {
super.initState();
_filtered = widget.contacts;
}
void _onSearch(String q) {
setState(() {
_filtered = widget.contacts
.where((c) => c.displayName.toLowerCase().contains(q.toLowerCase()))
.toList();
});
}
@override
Widget build(BuildContext context) {
return FractionallySizedBox(
heightFactor: 0.8,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: const InputDecoration(hintText: "電話帳から検索...", prefixIcon: Icon(Icons.search)),
onChanged: _onSearch,
),
),
Expanded(
child: ListView.builder(
itemCount: _filtered.length,
itemBuilder: (context, index) => ListTile(
title: Text(_filtered[index].displayName),
onTap: () => Navigator.pop(context, _filtered[index]),
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,249 @@
// lib/screens/invoice_detail_page.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.dart';
///
class InvoiceDetailPage extends StatefulWidget {
final Invoice invoice;
const InvoiceDetailPage({
Key? key,
required this.invoice,
}) : super(key: key);
@override
State<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
}
class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
late Invoice _currentInvoice;
final _descriptionController = TextEditingController();
final _quantityController = TextEditingController();
final _unitPriceController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_currentInvoice = widget.invoice;
}
@override
void dispose() {
_descriptionController.dispose();
_quantityController.dispose();
_unitPriceController.dispose();
super.dispose();
}
///
void _addItem() {
setState(() {
_currentInvoice = _currentInvoice.copyWith(
items: [
..._currentInvoice.items,
InvoiceItem(
description: "新規明細",
quantity: 1,
unitPrice: 10000,
)
]
);
});
}
///
void _removeItem(int index) {
setState(() {
final newItems = List<InvoiceItem>.from(_currentInvoice.items);
newItems.removeAt(index);
_currentInvoice = _currentInvoice.copyWith(items: newItems);
});
}
///
void _updateItem(int index, InvoiceItem item) {
setState(() {
final newItems = List<InvoiceItem>.from(_currentInvoice.items);
newItems[index] = item;
_currentInvoice = _currentInvoice.copyWith(items: newItems);
});
}
/// PDFを再生成
Future<void> _regeneratePdf() async {
setState(() => _isLoading = true);
final path = await generateInvoicePdf(_currentInvoice);
if (path != null) {
final updatedInvoice = _currentInvoice.copyWith(filePath: path);
// TODO: Repositoryで保存
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFを更新しました')),
);
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDFの生成に失敗しました')),
);
}
}
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
final amountFormatter = NumberFormat("#,###");
final dateFormatter = DateFormat('yyyy/MM/dd');
return Scaffold(
appBar: AppBar(
title: Text("${_currentInvoice.type.label}詳細"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
// TODO:
},
),
IconButton(
icon: const Icon(Icons.print),
onPressed: _regeneratePdf,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"基本情報",
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text("種類: ${_currentInvoice.type.label}"),
Text("顧客: ${_currentInvoice.customer.formalName}"),
Text("日付: ${dateFormatter.format(_currentInvoice.date)}"),
Text("番号: ${_currentInvoice.invoiceNumber}"),
if (_currentInvoice.notes != null)
Text("備考: ${_currentInvoice.notes}"),
],
),
),
),
const SizedBox(height: 16),
//
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"明細",
style: Theme.of(context).textTheme.titleLarge,
),
TextButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add),
label: const Text("明細追加"),
),
],
),
const SizedBox(height: 8),
..._currentInvoice.items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.description,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
"${item.quantity} × ¥${amountFormatter.format(item.unitPrice)}",
style: TextStyle(
color: item.isDiscount ? Colors.red : null,
decoration: item.isDiscount
? TextDecoration.lineThrough
: null,
),
),
],
),
),
Text(
"¥${amountFormatter.format(item.subtotal)}",
style: TextStyle(
fontWeight: FontWeight.bold,
color: item.isDiscount ? Colors.red : null,
),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _removeItem(index),
),
],
),
);
}).toList(),
],
),
),
),
const SizedBox(height: 16),
//
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("小計: ¥${amountFormatter.format(_currentInvoice.subtotal)}"),
Text("消費税: ¥${amountFormatter.format(_currentInvoice.tax)}"),
Text(
"合計: ¥${amountFormatter.format(_currentInvoice.totalAmount)}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/invoice_models.dart';
import '../services/invoice_repository.dart';
import 'invoice_detail_page.dart';
///
class InvoiceHistoryScreen extends StatefulWidget {
const InvoiceHistoryScreen({Key? key}) : super(key: key);
@override
State<InvoiceHistoryScreen> createState() => _InvoiceHistoryScreenState();
}
class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
final InvoiceRepository _repository = InvoiceRepository();
List<Invoice> _invoices = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadInvoices();
}
/// DBから履歴を読み込む
Future<void> _loadInvoices() async {
setState(() => _isLoading = true);
final data = await _repository.getAllInvoices();
setState(() {
_invoices = data;
_isLoading = false;
});
}
/// DBに紐付かないPDFファイルを一括削除
Future<void> _cleanupFiles() async {
final count = await _repository.cleanupOrphanedPdfs();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$count 個の不要なPDFファイルを削除しました')),
);
}
}
///
Future<void> _deleteInvoice(Invoice invoice) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text("削除の確認"),
content: Text("${invoice.type.label}番号: ${invoice.invoiceNumber}\nこのデータを削除しますか?\n(実体PDFファイルも削除されます)"),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text("削除する", style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirmed == true) {
await _repository.deleteInvoice(invoice);
_loadInvoices();
}
}
@override
Widget build(BuildContext context) {
final amountFormatter = NumberFormat("#,###");
final dateFormatter = DateFormat('yyyy/MM/dd HH:mm');
return Scaffold(
appBar: AppBar(
title: const Text("発行履歴管理"),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.cleaning_services),
tooltip: "ゴミファイルを掃除",
onPressed: _cleanupFiles,
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadInvoices,
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _invoices.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 64, color: Colors.grey.shade300),
const SizedBox(height: 16),
const Text("発行済みの帳票はありません", style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
itemCount: _invoices.length,
itemBuilder: (context, index) {
final invoice = _invoices[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: Stack(
alignment: Alignment.bottomRight,
children: [
const CircleAvatar(
backgroundColor: Colors.indigo,
child: Icon(Icons.description, color: Colors.white),
),
if (invoice.isShared)
Container(
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_circle,
color: Colors.green,
size: 18,
),
),
],
),
title: Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blueGrey.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
invoice.type.label,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
invoice.customer.formalName,
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("No: ${invoice.invoiceNumber}"),
Text(dateFormatter.format(invoice.date), style: const TextStyle(fontSize: 12)),
],
),
trailing: Text(
"¥${amountFormatter.format(invoice.totalAmount)}",
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.indigo,
fontSize: 16,
),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(invoice: invoice),
),
);
_loadInvoices();
},
onLongPress: () => _deleteInvoice(invoice),
),
);
},
),
);
}
}

View file

@ -0,0 +1,255 @@
// lib/screens/invoice_input_screen.dart
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/customer_model.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.dart';
import '../services/invoice_repository.dart';
import '../services/master_repository.dart';
import 'customer_picker_modal.dart';
///
class InvoiceInputForm extends StatefulWidget {
final Function(Invoice invoice, String filePath) onInvoiceGenerated;
const InvoiceInputForm({
Key? key,
required this.onInvoiceGenerated,
}) : super(key: key);
@override
State<InvoiceInputForm> createState() => _InvoiceInputFormState();
}
class _InvoiceInputFormState extends State<InvoiceInputForm> {
final _clientController = TextEditingController();
final _amountController = TextEditingController(text: "250000");
final _invoiceRepository = InvoiceRepository();
final _masterRepository = MasterRepository();
DocumentType _selectedType = DocumentType.invoice; //
String _status = "取引先を選択してPDFを生成してください";
List<Customer> _customerBuffer = [];
Customer? _selectedCustomer;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadInitialData();
}
///
Future<void> _loadInitialData() async {
setState(() => _isLoading = true);
final savedCustomers = await _masterRepository.loadCustomers();
setState(() {
_customerBuffer = savedCustomers;
if (_customerBuffer.isNotEmpty) {
_selectedCustomer = _customerBuffer.first;
_clientController.text = _selectedCustomer!.formalName;
}
_isLoading = false;
});
_invoiceRepository.cleanupOrphanedPdfs().then((count) {
if (count > 0) {
debugPrint('Cleaned up $count orphaned PDF files.');
}
});
}
@override
void dispose() {
_clientController.dispose();
_amountController.dispose();
super.dispose();
}
///
Future<void> _openCustomerPicker() async {
setState(() => _status = "顧客マスターを開いています...");
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => FractionallySizedBox(
heightFactor: 0.9,
child: CustomerPickerModal(
existingCustomers: _customerBuffer,
onCustomerSelected: (customer) async {
setState(() {
int index = _customerBuffer.indexWhere((c) => c.id == customer.id);
if (index != -1) {
_customerBuffer[index] = customer;
} else {
_customerBuffer.add(customer);
}
_selectedCustomer = customer;
_clientController.text = customer.formalName;
_status = "${customer.formalName}」を選択しました";
});
await _masterRepository.saveCustomers(_customerBuffer);
if (mounted) Navigator.pop(context);
},
onCustomerDeleted: (customer) async {
setState(() {
_customerBuffer.removeWhere((c) => c.id == customer.id);
if (_selectedCustomer?.id == customer.id) {
_selectedCustomer = null;
_clientController.clear();
}
});
await _masterRepository.saveCustomers(_customerBuffer);
},
),
),
);
}
/// PDFを生成して詳細画面へ進む
Future<void> _handleInitialGenerate() async {
if (_selectedCustomer == null) {
setState(() => _status = "取引先を選択してください");
return;
}
final unitPrice = int.tryParse(_amountController.text) ?? 0;
final initialItems = [
InvoiceItem(
description: "${_selectedType.label}",
quantity: 1,
unitPrice: unitPrice,
)
];
final invoice = Invoice(
customer: _selectedCustomer!,
date: DateTime.now(),
items: initialItems,
type: _selectedType,
);
setState(() => _status = "${_selectedType.label}を生成中...");
final path = await generateInvoicePdf(invoice);
if (path != null) {
final updatedInvoice = invoice.copyWith(filePath: path);
await _invoiceRepository.saveInvoice(updatedInvoice);
widget.onInvoiceGenerated(updatedInvoice, path);
setState(() => _status = "${_selectedType.label}を生成しDBに登録しました。");
} else {
setState(() => _status = "PDFの生成に失敗しました");
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"帳票の種類を選択",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
const SizedBox(height: 8),
Wrap(
spacing: 8.0,
children: DocumentType.values.map((type) {
return ChoiceChip(
label: Text(type.label),
selected: _selectedType == type,
onSelected: (selected) {
if (selected) {
setState(() => _selectedType = type);
}
},
selectedColor: Colors.indigo.shade100,
);
}).toList(),
),
const SizedBox(height: 24),
const Text(
"宛先と基本金額の設定",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blueGrey),
),
const SizedBox(height: 12),
Row(children: [
Expanded(
child: TextField(
controller: _clientController,
readOnly: true,
onTap: _openCustomerPicker,
decoration: const InputDecoration(
labelText: "取引先名 (タップして選択)",
hintText: "マスターから選択または電話帳から取り込み",
prefixIcon: Icon(Icons.business),
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.person_add_alt_1, color: Colors.indigo, size: 40),
onPressed: _openCustomerPicker,
tooltip: "顧客を選択・登録",
),
]),
const SizedBox(height: 16),
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "基本金額 (税抜)",
hintText: "明細の1行目として登録されます",
prefixIcon: Icon(Icons.currency_yen),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _handleInitialGenerate,
icon: const Icon(Icons.description),
label: Text("${_selectedType.label}を作成して詳細編集へ"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 60),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Text(
_status,
style: const TextStyle(fontSize: 12, color: Colors.black54),
textAlign: TextAlign.center,
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,274 @@
import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../data/product_master.dart';
import '../models/invoice_models.dart';
import '../services/master_repository.dart';
///
class ProductPickerModal extends StatefulWidget {
final Function(InvoiceItem) onItemSelected;
const ProductPickerModal({
Key? key,
required this.onItemSelected,
}) : super(key: key);
@override
State<ProductPickerModal> createState() => _ProductPickerModalState();
}
class _ProductPickerModalState extends State<ProductPickerModal> {
final MasterRepository _masterRepository = MasterRepository();
String _searchQuery = "";
List<Product> _masterProducts = [];
List<Product> _filteredProducts = [];
String _selectedCategory = "すべて";
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadProducts();
}
///
Future<void> _loadProducts() async {
setState(() => _isLoading = true);
final products = await _masterRepository.loadProducts();
setState(() {
_masterProducts = products;
_isLoading = false;
_filterProducts();
});
}
///
void _filterProducts() {
setState(() {
_filteredProducts = _masterProducts.where((product) {
final matchesQuery = product.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
product.id.toLowerCase().contains(_searchQuery.toLowerCase());
final matchesCategory = _selectedCategory == "すべて" || (product.category == _selectedCategory);
return matchesQuery && matchesCategory;
}).toList();
});
}
///
void _showProductEditDialog({Product? existingProduct}) {
final idController = TextEditingController(text: existingProduct?.id ?? "");
final nameController = TextEditingController(text: existingProduct?.name ?? "");
final priceController = TextEditingController(text: existingProduct?.defaultUnitPrice.toString() ?? "");
final categoryController = TextEditingController(text: existingProduct?.category ?? "");
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(existingProduct == null ? "新規商品の登録" : "商品情報の編集"),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (existingProduct == null)
TextField(
controller: idController,
decoration: const InputDecoration(labelText: "商品コード (例: S001)", border: OutlineInputBorder()),
),
const SizedBox(height: 12),
TextField(
controller: nameController,
decoration: const InputDecoration(labelText: "商品名", border: OutlineInputBorder()),
),
const SizedBox(height: 12),
TextField(
controller: priceController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: "標準単価", border: OutlineInputBorder()),
),
const SizedBox(height: 12),
TextField(
controller: categoryController,
decoration: const InputDecoration(labelText: "カテゴリ (任意)", border: OutlineInputBorder()),
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
ElevatedButton(
onPressed: () async {
final String name = nameController.text.trim();
final int price = int.tryParse(priceController.text) ?? 0;
if (name.isEmpty) return;
Product updatedProduct;
if (existingProduct != null) {
updatedProduct = existingProduct.copyWith(
name: name,
defaultUnitPrice: price,
category: categoryController.text.trim(),
);
} else {
updatedProduct = Product(
id: idController.text.isEmpty ? const Uuid().v4().substring(0, 8) : idController.text,
name: name,
defaultUnitPrice: price,
category: categoryController.text.trim(),
);
}
//
await _masterRepository.upsertProduct(updatedProduct);
if (mounted) {
Navigator.pop(context);
_loadProducts(); //
}
},
child: const Text("保存"),
),
],
),
);
}
///
void _confirmDelete(Product product) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("商品の削除"),
content: Text("${product.name}」をマスターから削除しますか?"),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
TextButton(
onPressed: () async {
setState(() {
_masterProducts.removeWhere((p) => p.id == product.id);
});
await _masterRepository.saveProducts(_masterProducts);
if (mounted) {
Navigator.pop(context);
_filterProducts();
}
},
child: const Text("削除する", style: TextStyle(color: Colors.red)),
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Material(child: Center(child: CircularProgressIndicator()));
}
final dynamicCategories = ["すべて", ..._masterProducts.map((p) => p.category ?? 'その他').toSet().toList()];
return Material(
color: Colors.white,
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("商品マスター管理", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
],
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
hintText: "商品名やコードで検索...",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
filled: true,
fillColor: Colors.grey.shade50,
),
onChanged: (val) {
_searchQuery = val;
_filterProducts();
},
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: dynamicCategories.map((cat) {
final isSelected = _selectedCategory == cat;
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ChoiceChip(
label: Text(cat),
selected: isSelected,
onSelected: (s) {
if (s) {
setState(() {
_selectedCategory = cat;
_filterProducts();
});
}
},
),
);
}).toList(),
),
),
),
const SizedBox(width: 8),
IconButton.filled(
onPressed: () => _showProductEditDialog(),
icon: const Icon(Icons.add),
tooltip: "新規商品を追加",
),
],
),
],
),
),
const Divider(height: 1),
Expanded(
child: _filteredProducts.isEmpty
? const Center(child: Text("該当する商品がありません"))
: ListView.separated(
itemCount: _filteredProducts.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final product = _filteredProducts[index];
return ListTile(
leading: const Icon(Icons.inventory_2, color: Colors.blueGrey),
title: Text(product.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text("${product.id} | ¥${product.defaultUnitPrice}"),
onTap: () => widget.onItemSelected(product.toInvoiceItem()),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_outlined, size: 20, color: Colors.blueGrey),
onPressed: () => _showProductEditDialog(existingProduct: product),
),
IconButton(
icon: const Icon(Icons.delete_outline, size: 20, color: Colors.redAccent),
onPressed: () => _confirmDelete(product),
),
],
),
);
},
),
),
],
),
);
}
}

View file

@ -0,0 +1,117 @@
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/invoice_models.dart';
/// DB
/// PDFファイルとデータの整合性を保つための機能を提供します
class InvoiceRepository {
static const String _dbFileName = 'invoices_db.json';
///
Future<File> _getDbFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_dbFileName');
}
///
Future<List<Invoice>> getAllInvoices() async {
try {
final file = await _getDbFile();
if (!await file.exists()) return [];
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((json) => Invoice.fromJson(json)).toList()
..sort((a, b) => b.date.compareTo(a.date)); //
} catch (e) {
print('DB Loading Error: $e');
return [];
}
}
///
Future<void> saveInvoice(Invoice invoice) async {
final List<Invoice> all = await getAllInvoices();
//
final index = all.indexWhere((i) => i.invoiceNumber == invoice.invoiceNumber);
if (index != -1) {
final oldInvoice = all[index];
final oldPath = oldInvoice.filePath;
//
if (oldPath != null && oldPath != invoice.filePath) {
//
if (!oldInvoice.isShared) {
await _deletePhysicalFile(oldPath);
} else {
print('Skipping deletion of shared file: $oldPath');
}
}
all[index] = invoice;
} else {
all.add(invoice);
}
final file = await _getDbFile();
await file.writeAsString(json.encode(all.map((i) => i.toJson()).toList()));
}
///
Future<void> deleteInvoice(Invoice invoice) async {
final List<Invoice> all = await getAllInvoices();
all.removeWhere((i) => i.invoiceNumber == invoice.invoiceNumber);
//
if (invoice.filePath != null) {
await _deletePhysicalFile(invoice.filePath!);
}
final file = await _getDbFile();
await file.writeAsString(json.encode(all.map((i) => i.toJson()).toList()));
}
/// PDFファイルをストレージから削除する
Future<void> _deletePhysicalFile(String path) async {
try {
final file = File(path);
if (await file.exists()) {
await file.delete();
print('Physical file deleted: $path');
}
} catch (e) {
print('File Deletion Error: $path, $e');
}
}
/// DBに登録されていないPDFファイル
/// DBエントリーのパスは
Future<int> cleanupOrphanedPdfs() async {
final List<Invoice> all = await getAllInvoices();
// DBに登録されている全ての有効なパス
final Set<String> registeredPaths = all
.where((i) => i.filePath != null)
.map((i) => i.filePath!)
.toSet();
final directory = await getExternalStorageDirectory();
if (directory == null) return 0;
int deletedCount = 0;
final List<FileSystemEntity> files = directory.listSync();
for (var entity in files) {
if (entity is File && entity.path.endsWith('.pdf')) {
// DBのどの請求データ
if (!registeredPaths.contains(entity.path)) {
await entity.delete();
deletedCount++;
}
}
}
return deletedCount;
}
}

View file

@ -0,0 +1,150 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import '../models/customer_model.dart';
import '../models/company_model.dart'; // Companyモデルをインポート
import '../data/product_master.dart';
///
class MasterRepository {
static const String _customerFileName = 'customers_master.json';
static const String _productFileName = 'products_master.json';
static const String _companyFileName = 'company_info.json'; //
///
Future<File> _getCustomerFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_customerFileName');
}
///
Future<File> _getProductFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_productFileName');
}
///
Future<File> _getCompanyFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_companyFileName');
}
// --- ---
///
Future<List<Customer>> loadCustomers() async {
try {
final file = await _getCustomerFile();
if (!await file.exists()) return [];
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((j) => Customer.fromJson(j)).toList();
} catch (e) {
debugPrint('Customer Master Loading Error: $e');
return [];
}
}
///
Future<void> saveCustomers(List<Customer> customers) async {
try {
final file = await _getCustomerFile();
final String encoded = json.encode(customers.map((c) => c.toJson()).toList());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Customer Master Saving Error: $e');
}
}
///
Future<void> upsertCustomer(Customer customer) async {
final customers = await loadCustomers();
final index = customers.indexWhere((c) => c.id == customer.id);
if (index != -1) {
customers[index] = customer;
} else {
customers.add(customer);
}
await saveCustomers(customers);
}
// --- ---
///
/// ProductMasterに定義された初期データを返す
Future<List<Product>> loadProducts() async {
try {
final file = await _getProductFile();
if (!await file.exists()) {
// ProductMasterのハードコードされたリストを返す
return List.from(ProductMaster.products);
}
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((j) => Product.fromJson(j)).toList();
} catch (e) {
debugPrint('Product Master Loading Error: $e');
return List.from(ProductMaster.products); //
}
}
///
Future<void> saveProducts(List<Product> products) async {
try {
final file = await _getProductFile();
final String encoded = json.encode(products.map((p) => p.toJson()).toList());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Product Master Saving Error: $e');
}
}
///
Future<void> upsertProduct(Product product) async {
final products = await loadProducts();
final index = products.indexWhere((p) => p.id == product.id);
if (index != -1) {
products[index] = product;
} else {
products.add(product);
}
await saveProducts(products);
}
// --- ---
///
/// Company.defaultCompany
Future<Company> loadCompany() async {
try {
final file = await _getCompanyFile();
if (!await file.exists()) {
return Company.defaultCompany;
}
final String content = await file.readAsString();
final Map<String, dynamic> jsonMap = json.decode(content);
return Company.fromJson(jsonMap);
} catch (e) {
debugPrint('Company Info Loading Error: $e');
return Company.defaultCompany; //
}
}
///
Future<void> saveCompany(Company company) async {
try {
final file = await _getCompanyFile();
final String encoded = json.encode(company.toJson());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Company Info Saving Error: $e');
}
}
}

View file

@ -0,0 +1,295 @@
// lib/services/pdf_generator.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart' show debugPrint;
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart';
import 'package:printing/printing.dart';
import '../models/invoice_models.dart';
import '../models/company_model.dart'; // Companyモデルをインポート
import 'master_repository.dart'; // MasterRepositoryをインポート
/// A4サイズのプロフェッショナルな帳票PDFを生成し
/// DocumentTypeに対応
Future<String?> generateInvoicePdf(Invoice invoice) async {
try {
final pdf = pw.Document();
//
final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData);
final boldTtf = pw.Font.ttf(fontData); // IPAexGはウェイトが1つなので同じものを使用
//
final MasterRepository masterRepository = MasterRepository();
final Company company = await masterRepository.loadCompany();
final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("¥#,###"); //
//
final String docTitle = invoice.type.label;
final String honorific = " 御中"; // (estimateでもinvoiceでも共通化)
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
theme: pw.ThemeData.withFont(base: ttf, bold: boldTtf),
build: (context) => [
//
pw.Header(
level: 0,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(docTitle, style: pw.TextStyle(fontSize: 28, fontWeight: pw.FontWeight.bold)),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text("管理番号: ${invoice.invoiceNumber}"),
pw.Text("発行日: ${dateFormatter.format(invoice.date)}"),
],
),
],
),
),
pw.SizedBox(height: 20),
//
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text("${invoice.customer.formalName}$honorific",
style: const pw.TextStyle(fontSize: 18)),
if (invoice.customer.department != null && invoice.customer.department!.isNotEmpty)
pw.Padding(
padding: const pw.EdgeInsets.only(top: 4),
child: pw.Text(invoice.customer.department!),
),
pw.SizedBox(height: 10),
pw.Text(invoice.type == DocumentType.estimate
? "下記の通り、御見積申し上げます。"
: "下記の通り、ご請求申し上げます。"),
],
),
),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(company.formalName, style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
if (company.zipCode != null && company.zipCode!.isNotEmpty) pw.Text(company.zipCode!),
if (company.address != null && company.address!.isNotEmpty) pw.Text(company.address!),
if (company.tel != null && company.tel!.isNotEmpty) pw.Text(company.tel!),
if (company.registrationNumber != null && company.registrationNumber!.isNotEmpty) pw.Text(company.registrationNumber! ),
],
),
),
],
),
pw.SizedBox(height: 30),
//
pw.Container(
padding: const pw.EdgeInsets.all(8),
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text("${docTitle}金額合計 (税込)", style: const pw.TextStyle(fontSize: 16)),
pw.Text("${amountFormatter.format(invoice.totalAmount)} -",
style: pw.TextStyle(fontSize: 20, fontWeight: pw.FontWeight.bold)),
],
),
),
pw.SizedBox(height: 20),
//
pw.TableHelper.fromTextArray(
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey300),
cellHeight: 30,
cellAlignments: {
0: pw.Alignment.centerLeft,
1: pw.Alignment.centerRight,
2: pw.Alignment.centerRight,
3: pw.Alignment.centerRight,
},
headers: ["品名 / 項目", "数量", "単価", "金額"],
data: List<List<String>>.generate(
invoice.items.length,
(index) {
final item = invoice.items[index];
return [
item.description,
item.quantity.toString(),
amountFormatter.format(item.unitPrice),
amountFormatter.format(item.subtotal),
];
},
),
),
//
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end,
children: [
pw.Container(
width: 200,
child: pw.Column(
children: [
pw.SizedBox(height: 10),
_buildSummaryRow("小計 (税抜)", amountFormatter.format(invoice.subtotal)),
_buildSummaryRow("消費税 (10%)", amountFormatter.format(invoice.tax)),
pw.Divider(),
_buildSummaryRow("合計", amountFormatter.format(invoice.totalAmount), isBold: true),
],
),
),
],
),
//
if (invoice.notes != null && invoice.notes!.isNotEmpty) ...[
pw.SizedBox(height: 40),
pw.Text("備考:", style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(8),
decoration: pw.BoxDecoration(border: pw.Border.all(color: PdfColors.grey400)),
child: pw.Text(invoice.notes!)),
],
],
footer: (context) => pw.Container(
alignment: pw.Alignment.centerRight,
margin: const pw.EdgeInsets.only(top: 16),
child: pw.Text(
"Page ${context.pageNumber} / ${context.pagesCount}",
style: const pw.TextStyle(color: PdfColors.grey),
),
),
),
);
final Uint8List bytes = await pdf.save();
final String hash = sha256.convert(bytes).toString().substring(0, 8);
final String dateFileStr = DateFormat('yyyyMMdd').format(invoice.date);
String fileName = "${invoice.type.name}_${dateFileStr}_${invoice.customer.formalName}_$hash.pdf";
final directory = await getExternalStorageDirectory();
if (directory == null) return null;
final file = File("${directory.path}/$fileName");
await file.writeAsBytes(bytes);
return file.path;
} catch (e) {
debugPrint("PDF Generation Error: $e");
return null;
}
}
/// 58mmレシートPDFを生成して印刷ダイアログを表示する
Future<void> printThermalReceipt(Invoice invoice) async {
try {
final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData);
final amountFormatter = NumberFormat("¥#,###"); //
//
final MasterRepository masterRepository = MasterRepository();
final Company company = await masterRepository.loadCompany();
final doc = pw.Document();
doc.addPage(
pw.Page(
// 58mm幅のサーマルプリンタ向け設定 (164pt)
pageFormat: const PdfPageFormat(58 * PdfPageFormat.mm, double.infinity, marginAll: 2 * PdfPageFormat.mm),
theme: pw.ThemeData.withFont(base: ttf),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Center(
child: pw.Text(invoice.type.label, style: pw.TextStyle(fontSize: 16, fontWeight: pw.FontWeight.bold)),
),
pw.SizedBox(height: 5),
pw.Text("${invoice.customer.formalName}", style: const pw.TextStyle(fontSize: 10)),
pw.Divider(thickness: 1, borderStyle: pw.BorderStyle.dashed),
pw.SizedBox(height: 5),
pw.Center(
child: pw.Text(amountFormatter.format(invoice.totalAmount),
style: pw.TextStyle(fontSize: 18, fontWeight: pw.FontWeight.bold)),
),
pw.Center(child: pw.Text("(うち消費税 ${amountFormatter.format(invoice.tax)})", style: const pw.TextStyle(fontSize: 8))),
pw.SizedBox(height: 10),
pw.Text("但し、お品代として", style: const pw.TextStyle(fontSize: 9)),
pw.Text("上記正に領収いたしました", style: const pw.TextStyle(fontSize: 9)),
pw.SizedBox(height: 10),
//
pw.Text("--- 明細 ---\n", style: const pw.TextStyle(fontSize: 8)),
...invoice.items.map((item) => pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Expanded(child: pw.Text(item.description, style: const pw.TextStyle(fontSize: 8))),
pw.Text("x${item.quantity} ", style: const pw.TextStyle(fontSize: 8)),
pw.Text(amountFormatter.format(item.subtotal), style: const pw.TextStyle(fontSize: 8)),
],
)),
pw.Divider(thickness: 0.5),
pw.SizedBox(height: 5),
pw.Align(
alignment: pw.Alignment.centerRight,
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(company.formalName, style: const pw.TextStyle(fontSize: 9)),
pw.Text(DateFormat('yyyy/MM/dd HH:mm').format(invoice.date), style: const pw.TextStyle(fontSize: 7)),
pw.Text("No: ${invoice.invoiceNumber}", style: const pw.TextStyle(fontSize: 7)),
],
),
),
pw.SizedBox(height: 10),
pw.Center(child: pw.Text("ありがとうございました", style: const pw.TextStyle(fontSize: 8))),
pw.SizedBox(height: 20), //
],
);
},
),
);
//
await Printing.layoutPdf(
onLayout: (PdfPageFormat format) async => doc.save(),
name: "${invoice.type.name}_${invoice.invoiceNumber}",
);
} catch (e) {
debugPrint("Thermal Print Error: $e");
}
}
pw.Widget _buildSummaryRow(String label, String value, {bool isBold = false}) {
final style = pw.TextStyle(fontSize: 12, fontWeight: isBold ? pw.FontWeight.bold : null);
return pw.Padding(
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(label, style: style),
pw.Text(value, style: style),
],
),
);
}

1
flutter.参考/linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

View file

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "gemi_invoice")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.gemi_invoice")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -0,0 +1,19 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <printing/printing_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) printing_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
printing_plugin_register_with_registrar(printing_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -0,0 +1,25 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
printing
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View file

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View file

@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "gemi_invoice");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "gemi_invoice");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View file

@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
flutter.参考/macos/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View file

@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View file

@ -0,0 +1,16 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import printing
import share_plus
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

Some files were not shown because too many files have changed in this diff Show more