diff --git a/app.log b/app.log index 1ac78ff..5a11f51 100644 --- a/app.log +++ b/app.log @@ -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: exception=SystemExit(0)> +Traceback (most recent call last): + File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 392, in + 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 "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", 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: exception=SystemExit(0)> +Traceback (most recent call last): + File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 395, in + 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 "", line 3, in __repr__ + File "/usr/lib/python3.12/dataclasses.py", line 262, in wrapper + result = user_function(self) + ^^^^^^^^^^^^^^^^^^^ + File "", 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: exception=SystemExit(0)> +Traceback (most recent call last): + File "/home/user/dev/h-1.flet.3/app_compiz_shortcuts.py", line 395, in + 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 diff --git a/app_compiz_shortcuts.py b/app_compiz_shortcuts.py index e76efe6..a553e67 100644 --- a/app_compiz_shortcuts.py +++ b/app_compiz_shortcuts.py @@ -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) ) diff --git a/app_dashboard_template.py b/app_dashboard_template.py new file mode 100644 index 0000000..b00538f --- /dev/null +++ b/app_dashboard_template.py @@ -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) diff --git a/app_flutter_style_dashboard.py b/app_flutter_style_dashboard.py new file mode 100644 index 0000000..c865381 --- /dev/null +++ b/app_flutter_style_dashboard.py @@ -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) diff --git a/app_slip_adaptive.py b/app_slip_adaptive.py new file mode 100644 index 0000000..96491c3 --- /dev/null +++ b/app_slip_adaptive.py @@ -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) diff --git a/app_slip_explorer.py b/app_slip_explorer.py new file mode 100644 index 0000000..82d0a0a --- /dev/null +++ b/app_slip_explorer.py @@ -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) diff --git a/app_slip_interactive.py b/app_slip_interactive.py new file mode 100644 index 0000000..e9e624e --- /dev/null +++ b/app_slip_interactive.py @@ -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) diff --git a/components/customer_picker.py b/components/customer_picker.py new file mode 100644 index 0000000..f40e5ad --- /dev/null +++ b/components/customer_picker.py @@ -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("新規顧客登録") diff --git a/components/pinch_handler.py b/components/pinch_handler.py new file mode 100644 index 0000000..82d21b2 --- /dev/null +++ b/components/pinch_handler.py @@ -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) diff --git a/flutter.参考/.gitignore b/flutter.参考/.gitignore new file mode 100644 index 0000000..3a91c4a --- /dev/null +++ b/flutter.参考/.gitignore @@ -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 diff --git a/flutter.参考/.metadata b/flutter.参考/.metadata new file mode 100644 index 0000000..26d3e69 --- /dev/null +++ b/flutter.参考/.metadata @@ -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' diff --git a/flutter.参考/README.md b/flutter.参考/README.md new file mode 100644 index 0000000..fb65c06 --- /dev/null +++ b/flutter.参考/README.md @@ -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. diff --git a/flutter.参考/aiderのお仕事.md b/flutter.参考/aiderのお仕事.md new file mode 100644 index 0000000..225c63e --- /dev/null +++ b/flutter.参考/aiderのお仕事.md @@ -0,0 +1,9 @@ +既存の lib/ 内のモデルを確認し、請求書(Invoice)と領収書(Receipt)のPDF出力機能を実装せよ。 + + printing パッケージを使用して、プレビュー画面とPDF保存機能を実装すること。 + + レイアウトは日本の商習慣に合わせた標準的なものとし、ロゴ、会社名、インボイス登録番号、明細、合計金額、登録日を表示すること。 + + 重要: PDFを生成する際、将来のOdoo連携のために、ファイル名には {会社ID}_{端末ID}_{連番}.pdf という命名規則を適用せよ。 + + 生成したPDFのバイナリを share_plus で外部(メールやLINE)に共有できるボタンをUIに追加せよ。 diff --git a/flutter.参考/analysis_options.yaml b/flutter.参考/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/flutter.参考/analysis_options.yaml @@ -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 diff --git a/flutter.参考/android/.gitignore b/flutter.参考/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/flutter.参考/android/.gitignore @@ -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 diff --git a/flutter.参考/android/app/build.gradle.kts b/flutter.参考/android/app/build.gradle.kts new file mode 100644 index 0000000..9c178e2 --- /dev/null +++ b/flutter.参考/android/app/build.gradle.kts @@ -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 = "../.." +} diff --git a/flutter.参考/android/app/src/debug/AndroidManifest.xml b/flutter.参考/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/flutter.参考/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/flutter.参考/android/app/src/main/AndroidManifest.xml b/flutter.参考/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29a2967 --- /dev/null +++ b/flutter.参考/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter.参考/android/app/src/main/kotlin/com/example/gemi_invoice/MainActivity.kt b/flutter.参考/android/app/src/main/kotlin/com/example/gemi_invoice/MainActivity.kt new file mode 100644 index 0000000..b2b225e --- /dev/null +++ b/flutter.参考/android/app/src/main/kotlin/com/example/gemi_invoice/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.gemi_invoice + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/flutter.参考/android/app/src/main/res/drawable-v21/launch_background.xml b/flutter.参考/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/flutter.参考/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter.参考/android/app/src/main/res/drawable/launch_background.xml b/flutter.参考/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/flutter.参考/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter.参考/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter.参考/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/flutter.参考/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/flutter.参考/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter.参考/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/flutter.参考/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/flutter.参考/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter.参考/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/flutter.参考/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/flutter.参考/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter.参考/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/flutter.参考/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/flutter.参考/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter.参考/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/flutter.参考/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/flutter.参考/android/app/src/main/res/values-night/styles.xml b/flutter.参考/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/flutter.参考/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/flutter.参考/android/app/src/main/res/values/styles.xml b/flutter.参考/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/flutter.参考/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/flutter.参考/android/app/src/profile/AndroidManifest.xml b/flutter.参考/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/flutter.参考/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/flutter.参考/android/build.gradle.kts b/flutter.参考/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/flutter.参考/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/flutter.参考/android/gradle.properties b/flutter.参考/android/gradle.properties new file mode 100644 index 0000000..fbee1d8 --- /dev/null +++ b/flutter.参考/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/flutter.参考/android/settings.gradle.kts b/flutter.参考/android/settings.gradle.kts new file mode 100644 index 0000000..ca7fe06 --- /dev/null +++ b/flutter.参考/android/settings.gradle.kts @@ -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") diff --git a/flutter.参考/assets/fonts/ipaexg.ttf b/flutter.参考/assets/fonts/ipaexg.ttf new file mode 100755 index 0000000..811b9c0 Binary files /dev/null and b/flutter.参考/assets/fonts/ipaexg.ttf differ diff --git a/flutter.参考/collectfluttercode.py b/flutter.参考/collectfluttercode.py new file mode 100644 index 0000000..33b606a --- /dev/null +++ b/flutter.参考/collectfluttercode.py @@ -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() diff --git a/flutter.参考/dir.txt b/flutter.参考/dir.txt new file mode 100644 index 0000000..7b9fe82 --- /dev/null +++ b/flutter.参考/dir.txt @@ -0,0 +1,1005 @@ +.: +合計 256 +drwxrwxr-x 1 user user 4232 2月 5 16:26 . +drwxrwxr-x 1 user user 1088 1月 31 22:18 .. +drwxrwxr-x 1 user user 1088 2月 2 16:45 .dart_tool +-rw-rw-r-- 1 user user 6591 2月 2 16:45 .flutter-plugins-dependencies +-rw-rw-r-- 1 user user 1596 1月 31 20:48 .gitignore +drwxrwxr-x 1 user user 712 1月 31 19:58 .idea +-rw-rw-r-- 1 user user 1706 1月 31 19:58 .metadata +-rw-rw-r-- 1 user user 555 1月 31 19:58 README.md +-rw-rw-r-- 1 user user 1420 1月 31 19:58 analysis_options.yaml +drwxrwxr-x 1 user user 2216 1月 31 20:30 android +drwxrwxr-x 1 user user 248 1月 31 11:16 assets +drwxrwxr-x 1 user user 504 2月 2 16:45 build +-rw-rw-r-- 1 user user 1585 2月 1 12:51 collectfluttercode.py +-rw-rw-r-- 1 user user 0 2月 5 16:26 dir.txt +-rw-rw-r-- 1 user user 107944 2月 1 12:52 flutter_bundle_for_ai.txt +-rw-rw-r-- 1 user user 842 1月 31 10:35 gemi_invoice.iml +drwxrwxr-x 1 user user 1088 1月 31 19:58 ios +drwxrwxr-x 1 user user 1008 1月 31 22:23 lib +drwxrwxr-x 1 user user 672 1月 31 19:58 linux +drwxrwxr-x 1 user user 1088 1月 31 19:58 macos +-rw-rw-r-- 1 user user 20401 1月 31 23:14 pubspec.lock +-rw-rw-r-- 1 user user 4160 1月 31 23:14 pubspec.yaml +drwxrwxr-x 1 user user 1776 2月 5 16:22 screenshot +drwxrwxr-x 1 user user 208 1月 31 19:58 test +drwxrwxr-x 1 user user 672 1月 31 19:58 web +drwxrwxr-x 1 user user 672 1月 31 19:58 windows + +./.dart_tool: +合計 64 +drwxrwxr-x 1 user user 1088 2月 2 16:45 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +drwxrwxr-x 1 user user 208 2月 2 16:45 dartpad +drwxrwxr-x 1 user user 456 2月 2 16:45 flutter_build +drwxrwxr-x 1 user user 336 2月 2 16:45 hooks_runner +-rw-rw-r-- 1 user user 16807 2月 2 16:45 package_config.json +-rw-rw-r-- 1 user user 14897 2月 2 16:45 package_graph.json +-rw-rw-r-- 1 user user 6 2月 2 16:45 version + +./.dart_tool/dartpad: +合計 12 +drwxrwxr-x 1 user user 208 2月 2 16:45 . +drwxrwxr-x 1 user user 1088 2月 2 16:45 .. +-rw-rw-r-- 1 user user 764 2月 2 16:45 web_plugin_registrant.dart + +./.dart_tool/flutter_build: +合計 16 +drwxrwxr-x 1 user user 456 2月 2 16:45 . +drwxrwxr-x 1 user user 1088 2月 2 16:45 .. +-rw-rw-r-- 1 user user 4918 2月 2 16:45 dart_plugin_registrant.dart +drwxrwxr-x 1 user user 2376 2月 2 16:45 e1dd0145fbc584d0722867d88ef93213 + +./.dart_tool/flutter_build/e1dd0145fbc584d0722867d88ef93213: +合計 74116 +drwxrwxr-x 1 user user 2376 2月 2 16:45 . +drwxrwxr-x 1 user user 456 2月 2 16:45 .. +-rw-rw-r-- 1 user user 1020 2月 2 16:45 .filecache +-rw-rw-r-- 1 user user 75565304 2月 2 16:45 app.dill +-rw-rw-r-- 1 user user 310 2月 2 16:45 dart_build.d +-rw-rw-r-- 1 user user 646 2月 2 16:45 dart_build.stamp +-rw-rw-r-- 1 user user 351 2月 2 16:45 dart_build_result.json +-rw-rw-r-- 1 user user 190 2月 2 16:45 gen_dart_plugin_registrant.stamp +-rw-rw-r-- 1 user user 26 2月 2 16:45 gen_localizations.stamp +-rw-rw-r-- 1 user user 118 2月 2 16:45 install_code_assets.d +-rw-rw-r-- 1 user user 444 2月 2 16:45 install_code_assets.stamp +-rw-rw-r-- 1 user user 188347 2月 2 16:45 kernel_snapshot_program.d +-rw-rw-r-- 1 user user 45 2月 2 16:45 native_assets.json +-rw-rw-r-- 1 user user 2 2月 2 16:45 outputs.json + +./.dart_tool/hooks_runner: +合計 0 +drwxrwxr-x 1 user user 336 2月 2 16:45 . +drwxrwxr-x 1 user user 1088 2月 2 16:45 .. +drwxrwxr-x 1 user user 744 2月 2 16:45 objective_c +drwxrwxr-x 1 user user 168 2月 2 16:45 shared + +./.dart_tool/hooks_runner/objective_c: +合計 0 +drwxrwxr-x 1 user user 744 2月 2 16:45 . +drwxrwxr-x 1 user user 336 2月 2 16:45 .. +drwxrwxr-x 1 user user 1840 2月 2 16:45 3895e37e565c9d9bdbfa0be7a8ffd914 +drwxrwxr-x 1 user user 1840 2月 2 16:45 89a218548b4f5a4298b6eecea4e77074 +drwxrwxr-x 1 user user 1840 2月 2 16:45 d7ddd93a61f2686f6d8481cb4e14a407 + +./.dart_tool/hooks_runner/objective_c/3895e37e565c9d9bdbfa0be7a8ffd914: +合計 10860 +drwxrwxr-x 1 user user 1840 2月 2 16:45 . +drwxrwxr-x 1 user user 744 2月 2 16:45 .. +-rw-rw-r-- 1 user user 215 2月 2 16:45 .lock +-rw-rw-r-- 1 user user 119 2月 2 16:45 dependencies.dependencies_hash_file.json +-rw-rw-r-- 1 user user 246 2月 2 16:45 hook.dependencies_hash_file.json +-rw-rw-r-- 1 user user 10998112 2月 2 16:45 hook.dill +-rw-rw-r-- 1 user user 19387 2月 2 16:45 hook.dill.d +-rw-rw-r-- 1 user user 1100 2月 2 16:45 input.json +drwxrwxr-x 1 user user 0 2月 2 16:45 out +-rw-rw-r-- 1 user user 95 2月 2 16:45 output.json +-rw-rw-r-- 1 user user 0 2月 2 16:45 stderr.txt +-rw-rw-r-- 1 user user 1454 2月 2 16:45 stdout.txt + +./.dart_tool/hooks_runner/objective_c/3895e37e565c9d9bdbfa0be7a8ffd914/out: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1840 2月 2 16:45 .. + +./.dart_tool/hooks_runner/objective_c/89a218548b4f5a4298b6eecea4e77074: +合計 10860 +drwxrwxr-x 1 user user 1840 2月 2 16:45 . +drwxrwxr-x 1 user user 744 2月 2 16:45 .. +-rw-rw-r-- 1 user user 215 2月 2 16:45 .lock +-rw-rw-r-- 1 user user 119 2月 2 16:45 dependencies.dependencies_hash_file.json +-rw-rw-r-- 1 user user 246 2月 2 16:45 hook.dependencies_hash_file.json +-rw-rw-r-- 1 user user 10998112 2月 2 16:45 hook.dill +-rw-rw-r-- 1 user user 19387 2月 2 16:45 hook.dill.d +-rw-rw-r-- 1 user user 1100 2月 2 16:45 input.json +drwxrwxr-x 1 user user 0 2月 2 16:45 out +-rw-rw-r-- 1 user user 95 2月 2 16:45 output.json +-rw-rw-r-- 1 user user 0 2月 2 16:45 stderr.txt +-rw-rw-r-- 1 user user 1454 2月 2 16:45 stdout.txt + +./.dart_tool/hooks_runner/objective_c/89a218548b4f5a4298b6eecea4e77074/out: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1840 2月 2 16:45 .. + +./.dart_tool/hooks_runner/objective_c/d7ddd93a61f2686f6d8481cb4e14a407: +合計 10860 +drwxrwxr-x 1 user user 1840 2月 2 16:45 . +drwxrwxr-x 1 user user 744 2月 2 16:45 .. +-rw-rw-r-- 1 user user 215 2月 2 16:45 .lock +-rw-rw-r-- 1 user user 119 2月 2 16:45 dependencies.dependencies_hash_file.json +-rw-rw-r-- 1 user user 246 2月 2 16:45 hook.dependencies_hash_file.json +-rw-rw-r-- 1 user user 10998112 2月 2 16:45 hook.dill +-rw-rw-r-- 1 user user 19387 2月 2 16:45 hook.dill.d +-rw-rw-r-- 1 user user 1102 2月 2 16:45 input.json +drwxrwxr-x 1 user user 0 2月 2 16:45 out +-rw-rw-r-- 1 user user 95 2月 2 16:45 output.json +-rw-rw-r-- 1 user user 0 2月 2 16:45 stderr.txt +-rw-rw-r-- 1 user user 1454 2月 2 16:45 stdout.txt + +./.dart_tool/hooks_runner/objective_c/d7ddd93a61f2686f6d8481cb4e14a407/out: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1840 2月 2 16:45 .. + +./.dart_tool/hooks_runner/shared: +合計 0 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 336 2月 2 16:45 .. +drwxrwxr-x 1 user user 336 2月 2 16:45 objective_c + +./.dart_tool/hooks_runner/shared/objective_c: +合計 12 +drwxrwxr-x 1 user user 336 2月 2 16:45 . +drwxrwxr-x 1 user user 168 2月 2 16:45 .. +-rw-rw-r-- 1 user user 215 2月 2 16:45 .lock +drwxrwxr-x 1 user user 1248 2月 2 16:45 build + +./.dart_tool/hooks_runner/shared/objective_c/build: +合計 0 +drwxrwxr-x 1 user user 1248 2月 2 16:45 . +drwxrwxr-x 1 user user 336 2月 2 16:45 .. +drwxrwxr-x 1 user user 0 2月 2 16:45 3895e37e56 +drwxrwxr-x 1 user user 0 2月 2 16:45 3895e37e565c9d9bdbfa0be7a8ffd914 +drwxrwxr-x 1 user user 0 2月 2 16:45 89a218548b +drwxrwxr-x 1 user user 0 2月 2 16:45 89a218548b4f5a4298b6eecea4e77074 +drwxrwxr-x 1 user user 0 2月 2 16:45 d7ddd93a61 +drwxrwxr-x 1 user user 0 2月 2 16:45 d7ddd93a61f2686f6d8481cb4e14a407 + +./.dart_tool/hooks_runner/shared/objective_c/build/3895e37e56: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1248 2月 2 16:45 .. + +./.dart_tool/hooks_runner/shared/objective_c/build/3895e37e565c9d9bdbfa0be7a8ffd914: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1248 2月 2 16:45 .. + +./.dart_tool/hooks_runner/shared/objective_c/build/89a218548b: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1248 2月 2 16:45 .. + +./.dart_tool/hooks_runner/shared/objective_c/build/89a218548b4f5a4298b6eecea4e77074: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1248 2月 2 16:45 .. + +./.dart_tool/hooks_runner/shared/objective_c/build/d7ddd93a61: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1248 2月 2 16:45 .. + +./.dart_tool/hooks_runner/shared/objective_c/build/d7ddd93a61f2686f6d8481cb4e14a407: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 1248 2月 2 16:45 .. + +./.idea: +合計 24 +drwxrwxr-x 1 user user 712 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +drwxrwxr-x 1 user user 376 1月 31 19:58 libraries +-rw-rw-r-- 1 user user 404 1月 31 19:58 modules.xml +drwxrwxr-x 1 user user 168 1月 31 19:58 runConfigurations +-rw-rw-r-- 1 user user 1517 1月 31 19:58 workspace.xml + +./.idea/libraries: +合計 24 +drwxrwxr-x 1 user user 376 1月 31 19:58 . +drwxrwxr-x 1 user user 712 1月 31 19:58 .. +-rw-rw-r-- 1 user user 1216 1月 31 19:58 Dart_SDK.xml +-rw-rw-r-- 1 user user 599 1月 31 19:58 KotlinJavaRuntime.xml + +./.idea/runConfigurations: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 712 1月 31 19:58 .. +-rw-rw-r-- 1 user user 271 1月 31 19:58 main_dart.xml + +./android: +合計 100 +drwxrwxr-x 1 user user 2216 1月 31 20:30 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 253 1月 31 19:58 .gitignore +drwxrwxr-x 1 user user 712 1月 31 20:31 .gradle +drwxrwxr-x 1 user user 168 1月 31 10:45 .kotlin +drwxrwxr-x 1 user user 376 1月 31 10:35 app +-rw-rw-r-- 1 user user 537 1月 31 19:58 build.gradle.kts +-rw-rw-r-- 1 user user 1601 1月 31 19:58 gemi_invoice_android.iml +drwxrwxr-x 1 user user 168 1月 31 20:28 gradle +-rw-rw-r-- 1 user user 138 1月 31 19:58 gradle.properties +-rwxrwxr-x 1 user user 4971 1月 31 19:58 gradlew +-rw-rw-r-- 1 user user 2404 1月 31 19:58 gradlew.bat +-rw-rw-r-- 1 user user 153 1月 31 19:59 local.properties +-rw-rw-r-- 1 user user 772 1月 31 19:58 settings.gradle.kts + +./android/.gradle: +合計 0 +drwxrwxr-x 1 user user 712 1月 31 20:31 . +drwxrwxr-x 1 user user 2216 1月 31 20:30 .. +drwxrwxr-x 1 user user 1216 1月 31 20:35 8.13 +drwxrwxr-x 1 user user 584 1月 31 20:35 buildOutputCleanup +drwxrwxr-x 1 user user 168 1月 31 20:31 noVersion +drwxrwxr-x 1 user user 168 1月 31 20:33 vcs-1 + +./android/.gradle/8.13: +合計 8 +drwxrwxr-x 1 user user 1216 1月 31 20:35 . +drwxrwxr-x 1 user user 712 1月 31 20:31 .. +drwxrwxr-x 1 user user 584 1月 31 23:17 checksums +drwxrwxr-x 1 user user 416 1月 31 20:35 executionHistory +drwxrwxr-x 1 user user 0 1月 31 20:30 expanded +drwxrwxr-x 1 user user 168 1月 31 20:33 fileChanges +drwxrwxr-x 1 user user 544 1月 31 20:31 fileHashes +-rw-rw-r-- 1 user user 0 2月 1 23:52 gc.properties +drwxrwxr-x 1 user user 0 1月 31 20:30 vcsMetadata + +./android/.gradle/8.13/checksums: +合計 80 +drwxrwxr-x 1 user user 584 1月 31 23:17 . +drwxrwxr-x 1 user user 1216 1月 31 20:35 .. +-rw-rw-r-- 1 user user 17 2月 2 16:45 checksums.lock +-rw-rw-r-- 1 user user 21997 1月 31 23:17 md5-checksums.bin +-rw-rw-r-- 1 user user 28217 1月 31 23:17 sha1-checksums.bin + +./android/.gradle/8.13/executionHistory: +合計 2336 +drwxrwxr-x 1 user user 416 1月 31 20:35 . +drwxrwxr-x 1 user user 1216 1月 31 20:35 .. +-rw-rw-r-- 1 user user 2367643 1月 31 23:43 executionHistory.bin +-rw-rw-r-- 1 user user 17 2月 2 16:45 executionHistory.lock + +./android/.gradle/8.13/expanded: +合計 0 +drwxrwxr-x 1 user user 0 1月 31 20:30 . +drwxrwxr-x 1 user user 1216 1月 31 20:35 .. + +./android/.gradle/8.13/fileChanges: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 20:33 . +drwxrwxr-x 1 user user 1216 1月 31 20:35 .. +-rw-rw-r-- 1 user user 1 2月 2 16:45 last-build.bin + +./android/.gradle/8.13/fileHashes: +合計 204 +drwxrwxr-x 1 user user 544 1月 31 20:31 . +drwxrwxr-x 1 user user 1216 1月 31 20:35 .. +-rw-rw-r-- 1 user user 151033 2月 2 16:45 fileHashes.bin +-rw-rw-r-- 1 user user 17 2月 2 16:45 fileHashes.lock +-rw-rw-r-- 1 user user 26827 1月 31 23:33 resourceHashesCache.bin + +./android/.gradle/8.13/vcsMetadata: +合計 0 +drwxrwxr-x 1 user user 0 1月 31 20:30 . +drwxrwxr-x 1 user user 1216 1月 31 20:35 .. + +./android/.gradle/buildOutputCleanup: +合計 120 +drwxrwxr-x 1 user user 584 1月 31 20:35 . +drwxrwxr-x 1 user user 712 1月 31 20:31 .. +-rw-rw-r-- 1 user user 17 2月 2 16:45 buildOutputCleanup.lock +-rw-rw-r-- 1 user user 50 1月 31 20:30 cache.properties +-rw-rw-r-- 1 user user 89495 2月 2 16:45 outputFiles.bin + +./android/.gradle/noVersion: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 20:31 . +drwxrwxr-x 1 user user 712 1月 31 20:31 .. +-rw-rw-r-- 1 user user 17 2月 2 16:45 buildLogic.lock + +./android/.gradle/vcs-1: +合計 8 +drwxrwxr-x 1 user user 168 1月 31 20:33 . +drwxrwxr-x 1 user user 712 1月 31 20:31 .. +-rw-rw-r-- 1 user user 0 2月 1 23:52 gc.properties + +./android/.kotlin: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:45 . +drwxrwxr-x 1 user user 2216 1月 31 20:30 .. +drwxrwxr-x 1 user user 248 1月 31 23:33 sessions + +./android/.kotlin/sessions: +合計 0 +drwxrwxr-x 1 user user 248 1月 31 23:33 . +drwxrwxr-x 1 user user 168 1月 31 10:45 .. + +./android/app: +合計 12 +drwxrwxr-x 1 user user 376 1月 31 10:35 . +drwxrwxr-x 1 user user 2216 1月 31 20:30 .. +-rw-rw-r-- 1 user user 1396 1月 31 20:27 build.gradle.kts +drwxrwxr-x 1 user user 504 1月 31 10:35 src + +./android/app/src: +合計 0 +drwxrwxr-x 1 user user 504 1月 31 10:35 . +drwxrwxr-x 1 user user 376 1月 31 10:35 .. +drwxrwxr-x 1 user user 208 1月 31 10:35 debug +drwxrwxr-x 1 user user 712 1月 31 10:35 main +drwxrwxr-x 1 user user 208 1月 31 10:35 profile + +./android/app/src/debug: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 10:35 . +drwxrwxr-x 1 user user 504 1月 31 10:35 .. +-rw-rw-r-- 1 user user 378 1月 31 10:35 AndroidManifest.xml + +./android/app/src/main: +合計 12 +drwxrwxr-x 1 user user 712 1月 31 10:35 . +drwxrwxr-x 1 user user 504 1月 31 10:35 .. +-rw-rw-r-- 1 user user 1944 1月 31 16:37 AndroidManifest.xml +drwxrwxr-x 1 user user 168 1月 31 10:35 java +drwxrwxr-x 1 user user 168 1月 31 10:35 kotlin +drwxrwxr-x 1 user user 1512 1月 31 10:35 res + +./android/app/src/main/java: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 712 1月 31 10:35 .. +drwxrwxr-x 1 user user 168 1月 31 10:35 io + +./android/app/src/main/java/io: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 168 1月 31 10:35 .. +drwxrwxr-x 1 user user 168 1月 31 10:35 flutter + +./android/app/src/main/java/io/flutter: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 168 1月 31 10:35 .. +drwxrwxr-x 1 user user 208 1月 31 10:35 plugins + +./android/app/src/main/java/io/flutter/plugins: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 10:35 . +drwxrwxr-x 1 user user 168 1月 31 10:35 .. +-rw-rw-r-- 1 user user 2267 1月 31 23:14 GeneratedPluginRegistrant.java + +./android/app/src/main/kotlin: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 712 1月 31 10:35 .. +drwxrwxr-x 1 user user 168 1月 31 10:35 com + +./android/app/src/main/kotlin/com: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 168 1月 31 10:35 .. +drwxrwxr-x 1 user user 168 1月 31 10:35 example + +./android/app/src/main/kotlin/com/example: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 168 1月 31 10:35 .. +drwxrwxr-x 1 user user 168 1月 31 10:35 gemi_invoice + +./android/app/src/main/kotlin/com/example/gemi_invoice: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 168 1月 31 10:35 .. +-rw-rw-r-- 1 user user 126 1月 31 10:35 MainActivity.kt + +./android/app/src/main/res: +合計 0 +drwxrwxr-x 1 user user 1512 1月 31 10:35 . +drwxrwxr-x 1 user user 712 1月 31 10:35 .. +drwxrwxr-x 1 user user 208 1月 31 10:35 drawable +drwxrwxr-x 1 user user 208 1月 31 10:35 drawable-v21 +drwxrwxr-x 1 user user 168 1月 31 10:35 mipmap-hdpi +drwxrwxr-x 1 user user 168 1月 31 10:35 mipmap-mdpi +drwxrwxr-x 1 user user 168 1月 31 10:35 mipmap-xhdpi +drwxrwxr-x 1 user user 168 1月 31 10:35 mipmap-xxhdpi +drwxrwxr-x 1 user user 168 1月 31 10:35 mipmap-xxxhdpi +drwxrwxr-x 1 user user 168 1月 31 10:35 values +drwxrwxr-x 1 user user 168 1月 31 10:35 values-night + +./android/app/src/main/res/drawable: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 434 1月 31 10:35 launch_background.xml + +./android/app/src/main/res/drawable-v21: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 438 1月 31 10:35 launch_background.xml + +./android/app/src/main/res/mipmap-hdpi: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 544 1月 31 10:35 ic_launcher.png + +./android/app/src/main/res/mipmap-mdpi: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 442 1月 31 10:35 ic_launcher.png + +./android/app/src/main/res/mipmap-xhdpi: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 721 1月 31 10:35 ic_launcher.png + +./android/app/src/main/res/mipmap-xxhdpi: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 1031 1月 31 10:35 ic_launcher.png + +./android/app/src/main/res/mipmap-xxxhdpi: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 1443 1月 31 10:35 ic_launcher.png + +./android/app/src/main/res/values: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 996 1月 31 10:35 styles.xml + +./android/app/src/main/res/values-night: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 10:35 . +drwxrwxr-x 1 user user 1512 1月 31 10:35 .. +-rw-rw-r-- 1 user user 995 1月 31 10:35 styles.xml + +./android/app/src/profile: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 10:35 . +drwxrwxr-x 1 user user 504 1月 31 10:35 .. +-rw-rw-r-- 1 user user 378 1月 31 10:35 AndroidManifest.xml + +./android/gradle: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 20:28 . +drwxrwxr-x 1 user user 2216 1月 31 20:30 .. +drwxrwxr-x 1 user user 416 1月 31 20:28 wrapper + +./android/gradle/wrapper: +合計 76 +drwxrwxr-x 1 user user 416 1月 31 20:28 . +drwxrwxr-x 1 user user 168 1月 31 20:28 .. +-rwxrwxr-x 1 user user 53636 1月 31 20:28 gradle-wrapper.jar +-rw-rw-r-- 1 user user 201 1月 31 20:28 gradle-wrapper.properties + +./assets: +合計 0 +drwxrwxr-x 1 user user 248 1月 31 11:16 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +drwxrwxr-x 1 user user 0 1月 31 11:21 fonts + +./assets/fonts: +合計 5968 +drwxrwxr-x 1 user user 0 1月 31 11:21 . +drwxrwxr-x 1 user user 248 1月 31 11:16 .. +-rwx------ 1 user user 6099900 4月 26 2019 ipaexg.ttf + +./build: +合計 0 +drwxrwxr-x 1 user user 504 2月 2 16:45 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +drwxrwxr-x 1 user user 168 2月 2 16:45 app +drwxrwxr-x 1 user user 168 2月 2 16:45 native_assets +drwxrwxr-x 1 user user 168 2月 2 16:45 reports + +./build/app: +合計 0 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 504 2月 2 16:45 .. +drwxrwxr-x 1 user user 168 2月 2 16:45 intermediates + +./build/app/intermediates: +合計 0 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 168 2月 2 16:45 .. +drwxrwxr-x 1 user user 168 2月 2 16:45 flutter + +./build/app/intermediates/flutter: +合計 0 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 168 2月 2 16:45 .. +drwxrwxr-x 1 user user 168 2月 2 16:45 debug + +./build/app/intermediates/flutter/debug: +合計 12 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 168 2月 2 16:45 .. +-rw-rw-r-- 1 user user 32 2月 2 16:45 .last_build_id + +./build/native_assets: +合計 0 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 504 2月 2 16:45 .. +drwxrwxr-x 1 user user 0 2月 2 16:45 android + +./build/native_assets/android: +合計 0 +drwxrwxr-x 1 user user 0 2月 2 16:45 . +drwxrwxr-x 1 user user 168 2月 2 16:45 .. + +./build/reports: +合計 0 +drwxrwxr-x 1 user user 168 2月 2 16:45 . +drwxrwxr-x 1 user user 504 2月 2 16:45 .. +drwxrwxr-x 1 user user 208 2月 2 16:45 problems + +./build/reports/problems: +合計 156 +drwxrwxr-x 1 user user 208 2月 2 16:45 . +drwxrwxr-x 1 user user 168 2月 2 16:45 .. +-rw-rw-r-- 1 user user 147493 2月 2 16:45 problems-report.html + +./ios: +合計 12 +drwxrwxr-x 1 user user 1088 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 569 1月 31 19:58 .gitignore +drwxrwxr-x 1 user user 1168 2月 2 16:45 Flutter +drwxrwxr-x 1 user user 1336 1月 31 19:58 Runner +drwxrwxr-x 1 user user 544 1月 31 19:58 Runner.xcodeproj +drwxrwxr-x 1 user user 376 1月 31 19:58 Runner.xcworkspace +drwxrwxr-x 1 user user 208 1月 31 19:58 RunnerTests + +./ios/Flutter: +合計 60 +drwxrwxr-x 1 user user 1168 2月 2 16:45 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 774 1月 31 19:58 AppFrameworkInfo.plist +-rw-rw-r-- 1 user user 30 1月 31 19:58 Debug.xcconfig +-rw-rw-r-- 1 user user 520 2月 2 16:45 Generated.xcconfig +-rw-rw-r-- 1 user user 30 1月 31 19:58 Release.xcconfig +drwxrwxr-x 1 user user 416 2月 2 16:45 ephemeral +-rwxr-xr-x 1 user user 550 2月 2 16:45 flutter_export_environment.sh + +./ios/Flutter/ephemeral: +合計 24 +drwxrwxr-x 1 user user 416 2月 2 16:45 . +drwxrwxr-x 1 user user 1168 2月 2 16:45 .. +-rw-rw-r-- 1 user user 1276 2月 2 16:45 flutter_lldb_helper.py +-rw-rw-r-- 1 user user 108 2月 2 16:45 flutter_lldbinit + +./ios/Runner: +合計 60 +drwxrwxr-x 1 user user 1336 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 391 1月 31 19:58 AppDelegate.swift +drwxrwxr-x 1 user user 416 1月 31 19:58 Assets.xcassets +drwxrwxr-x 1 user user 376 1月 31 19:58 Base.lproj +-rw-rw-r-- 1 user user 378 1月 31 15:38 GeneratedPluginRegistrant.h +-rw-rw-r-- 1 user user 1655 1月 31 23:14 GeneratedPluginRegistrant.m +-rw-rw-r-- 1 user user 1651 1月 31 19:58 Info.plist +-rw-rw-r-- 1 user user 38 1月 31 19:58 Runner-Bridging-Header.h + +./ios/Runner/Assets.xcassets: +合計 0 +drwxrwxr-x 1 user user 416 1月 31 19:58 . +drwxrwxr-x 1 user user 1336 1月 31 19:58 .. +drwxrwxr-x 1 user user 3288 1月 31 19:58 AppIcon.appiconset +drwxrwxr-x 1 user user 920 1月 31 19:58 LaunchImage.imageset + +./ios/Runner/Assets.xcassets/AppIcon.appiconset: +合計 200 +drwxrwxr-x 1 user user 3288 1月 31 19:58 . +drwxrwxr-x 1 user user 416 1月 31 19:58 .. +-rw-rw-r-- 1 user user 2519 1月 31 19:58 Contents.json +-rw-rw-r-- 1 user user 10932 1月 31 19:58 Icon-App-1024x1024@1x.png +-rw-rw-r-- 1 user user 295 1月 31 19:58 Icon-App-20x20@1x.png +-rw-rw-r-- 1 user user 406 1月 31 19:58 Icon-App-20x20@2x.png +-rw-rw-r-- 1 user user 450 1月 31 19:58 Icon-App-20x20@3x.png +-rw-rw-r-- 1 user user 282 1月 31 19:58 Icon-App-29x29@1x.png +-rw-rw-r-- 1 user user 462 1月 31 19:58 Icon-App-29x29@2x.png +-rw-rw-r-- 1 user user 704 1月 31 19:58 Icon-App-29x29@3x.png +-rw-rw-r-- 1 user user 406 1月 31 19:58 Icon-App-40x40@1x.png +-rw-rw-r-- 1 user user 586 1月 31 19:58 Icon-App-40x40@2x.png +-rw-rw-r-- 1 user user 862 1月 31 19:58 Icon-App-40x40@3x.png +-rw-rw-r-- 1 user user 862 1月 31 19:58 Icon-App-60x60@2x.png +-rw-rw-r-- 1 user user 1674 1月 31 19:58 Icon-App-60x60@3x.png +-rw-rw-r-- 1 user user 762 1月 31 19:58 Icon-App-76x76@1x.png +-rw-rw-r-- 1 user user 1226 1月 31 19:58 Icon-App-76x76@2x.png +-rw-rw-r-- 1 user user 1418 1月 31 19:58 Icon-App-83.5x83.5@2x.png + +./ios/Runner/Assets.xcassets/LaunchImage.imageset: +合計 60 +drwxrwxr-x 1 user user 920 1月 31 19:58 . +drwxrwxr-x 1 user user 416 1月 31 19:58 .. +-rw-rw-r-- 1 user user 391 1月 31 19:58 Contents.json +-rw-rw-r-- 1 user user 68 1月 31 19:58 LaunchImage.png +-rw-rw-r-- 1 user user 68 1月 31 19:58 LaunchImage@2x.png +-rw-rw-r-- 1 user user 68 1月 31 19:58 LaunchImage@3x.png +-rw-rw-r-- 1 user user 336 1月 31 19:58 README.md + +./ios/Runner/Base.lproj: +合計 24 +drwxrwxr-x 1 user user 376 1月 31 19:58 . +drwxrwxr-x 1 user user 1336 1月 31 19:58 .. +-rw-rw-r-- 1 user user 2377 1月 31 19:58 LaunchScreen.storyboard +-rw-rw-r-- 1 user user 1605 1月 31 19:58 Main.storyboard + +./ios/Runner.xcodeproj: +合計 32 +drwxrwxr-x 1 user user 544 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 23672 1月 31 19:58 project.pbxproj +drwxrwxr-x 1 user user 376 1月 31 19:58 project.xcworkspace +drwxrwxr-x 1 user user 168 1月 31 19:58 xcshareddata + +./ios/Runner.xcodeproj/project.xcworkspace: +合計 12 +drwxrwxr-x 1 user user 376 1月 31 19:58 . +drwxrwxr-x 1 user user 544 1月 31 19:58 .. +-rw-rw-r-- 1 user user 135 1月 31 19:58 contents.xcworkspacedata +drwxrwxr-x 1 user user 416 1月 31 19:58 xcshareddata + +./ios/Runner.xcodeproj/project.xcworkspace/xcshareddata: +合計 24 +drwxrwxr-x 1 user user 416 1月 31 19:58 . +drwxrwxr-x 1 user user 376 1月 31 19:58 .. +-rw-rw-r-- 1 user user 238 1月 31 19:58 IDEWorkspaceChecks.plist +-rw-rw-r-- 1 user user 226 1月 31 19:58 WorkspaceSettings.xcsettings + +./ios/Runner.xcodeproj/xcshareddata: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 544 1月 31 19:58 .. +drwxrwxr-x 1 user user 168 1月 31 19:58 xcschemes + +./ios/Runner.xcodeproj/xcshareddata/xcschemes: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 168 1月 31 19:58 .. +-rw-rw-r-- 1 user user 3833 1月 31 19:58 Runner.xcscheme + +./ios/Runner.xcworkspace: +合計 12 +drwxrwxr-x 1 user user 376 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 152 1月 31 19:58 contents.xcworkspacedata +drwxrwxr-x 1 user user 416 1月 31 19:58 xcshareddata + +./ios/Runner.xcworkspace/xcshareddata: +合計 24 +drwxrwxr-x 1 user user 416 1月 31 19:58 . +drwxrwxr-x 1 user user 376 1月 31 19:58 .. +-rw-rw-r-- 1 user user 238 1月 31 19:58 IDEWorkspaceChecks.plist +-rw-rw-r-- 1 user user 226 1月 31 19:58 WorkspaceSettings.xcsettings + +./ios/RunnerTests: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 285 1月 31 19:58 RunnerTests.swift + +./lib: +合計 28 +drwxrwxr-x 1 user user 1008 1月 31 22:23 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +drwxrwxr-x 1 user user 208 1月 31 22:23 data +-rw-rw-r-- 1 user user 4088 1月 31 23:22 main.dart +-rw-rw-r-- 1 user user 4805 1月 31 10:37 main.dart.org +drwxrwxr-x 1 user user 624 1月 31 23:21 models +drwxrwxr-x 1 user user 1248 2月 1 19:35 screens +drwxrwxr-x 1 user user 624 1月 31 23:09 services + +./lib/data: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 22:23 . +drwxrwxr-x 1 user user 1008 1月 31 22:23 .. +-rw-rw-r-- 1 user user 3237 1月 31 22:53 product_master.dart + +./lib/models: +合計 40 +drwxrwxr-x 1 user user 624 1月 31 23:21 . +drwxrwxr-x 1 user user 1008 1月 31 22:23 .. +-rw-rw-r-- 1 user user 3273 1月 31 23:21 company_model.dart +-rw-rw-r-- 1 user user 2838 1月 31 22:46 customer_model.dart +-rw-rw-r-- 1 user user 5319 2月 1 11:43 invoice_models.dart + +./lib/screens: +合計 104 +drwxrwxr-x 1 user user 1248 2月 1 19:35 . +drwxrwxr-x 1 user user 1008 1月 31 22:23 .. +-rw-rw-r-- 1 user user 8403 1月 31 23:22 company_editor_screen.dart +-rw-rw-r-- 1 user user 11572 1月 31 22:55 customer_picker_modal.dart +-rw-rw-r-- 1 user user 0 2月 1 19:35 invoice_detail_page.dart +-rw-rw-r-- 1 user user 7289 1月 31 23:12 invoice_history_screen.dart +-rw-rw-r-- 1 user user 8700 1月 31 23:11 invoice_input_screen.dart +-rw-rw-r-- 1 user user 10756 1月 31 23:09 product_picker_modal.dart + +./lib/services: +合計 56 +drwxrwxr-x 1 user user 624 1月 31 23:09 . +drwxrwxr-x 1 user user 1008 1月 31 22:23 .. +-rw-rw-r-- 1 user user 4143 1月 31 23:06 invoice_repository.dart +-rw-rw-r-- 1 user user 5127 1月 31 23:21 master_repository.dart +-rw-rw-r-- 1 user user 12454 1月 31 23:40 pdf_generator.dart + +./linux: +合計 28 +drwxrwxr-x 1 user user 672 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 18 1月 31 19:58 .gitignore +-rw-rw-r-- 1 user user 4763 1月 31 19:58 CMakeLists.txt +drwxrwxr-x 1 user user 960 2月 2 16:45 flutter +drwxrwxr-x 1 user user 752 1月 31 19:58 runner + +./linux/flutter: +合計 48 +drwxrwxr-x 1 user user 960 2月 2 16:45 . +drwxrwxr-x 1 user user 672 1月 31 19:58 .. +-rw-rw-r-- 1 user user 2815 1月 31 19:58 CMakeLists.txt +drwxrwxr-x 1 user user 208 2月 2 16:45 ephemeral +-rw-rw-r-- 1 user user 666 1月 31 23:14 generated_plugin_registrant.cc +-rw-rw-r-- 1 user user 303 1月 31 15:38 generated_plugin_registrant.h +-rw-rw-r-- 1 user user 771 1月 31 23:14 generated_plugins.cmake + +./linux/flutter/ephemeral: +合計 0 +drwxrwxr-x 1 user user 208 2月 2 16:45 . +drwxrwxr-x 1 user user 960 2月 2 16:45 .. +drwxrwxr-x 1 user user 752 2月 2 16:45 .plugin_symlinks + +./linux/flutter/ephemeral/.plugin_symlinks: +合計 0 +drwxrwxr-x 1 user user 752 2月 2 16:45 . +drwxrwxr-x 1 user user 208 2月 2 16:45 .. +lrwxrwxrwx 1 user user 63 2月 2 16:45 path_provider_linux -> /home/user/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +lrwxrwxrwx 1 user user 53 2月 2 16:45 printing -> /home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/ +lrwxrwxrwx 1 user user 55 2月 2 16:45 share_plus -> /home/user/.pub-cache/hosted/pub.dev/share_plus-12.0.1/ +lrwxrwxrwx 1 user user 62 2月 2 16:45 url_launcher_linux -> /home/user/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/ + +./linux/runner: +合計 52 +drwxrwxr-x 1 user user 752 1月 31 19:58 . +drwxrwxr-x 1 user user 672 1月 31 19:58 .. +-rw-rw-r-- 1 user user 974 1月 31 19:58 CMakeLists.txt +-rw-rw-r-- 1 user user 180 1月 31 19:58 main.cc +-rw-rw-r-- 1 user user 5463 1月 31 19:58 my_application.cc +-rw-rw-r-- 1 user user 451 1月 31 19:58 my_application.h + +./macos: +合計 12 +drwxrwxr-x 1 user user 1088 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 89 1月 31 19:58 .gitignore +drwxrwxr-x 1 user user 792 2月 2 16:45 Flutter +drwxrwxr-x 1 user user 1504 1月 31 19:58 Runner +drwxrwxr-x 1 user user 544 1月 31 19:58 Runner.xcodeproj +drwxrwxr-x 1 user user 376 1月 31 19:58 Runner.xcworkspace +drwxrwxr-x 1 user user 208 1月 31 19:58 RunnerTests + +./macos/Flutter: +合計 36 +drwxrwxr-x 1 user user 792 2月 2 16:45 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 48 1月 31 19:58 Flutter-Debug.xcconfig +-rw-rw-r-- 1 user user 48 1月 31 19:58 Flutter-Release.xcconfig +-rw-rw-r-- 1 user user 468 1月 31 23:14 GeneratedPluginRegistrant.swift +drwxrwxr-x 1 user user 416 2月 2 16:45 ephemeral + +./macos/Flutter/ephemeral: +合計 24 +drwxrwxr-x 1 user user 416 2月 2 16:45 . +drwxrwxr-x 1 user user 792 2月 2 16:45 .. +-rw-rw-r-- 1 user user 413 2月 2 16:45 Flutter-Generated.xcconfig +-rwxr-xr-x 1 user user 512 2月 2 16:45 flutter_export_environment.sh + +./macos/Runner: +合計 60 +drwxrwxr-x 1 user user 1504 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 311 1月 31 19:58 AppDelegate.swift +drwxrwxr-x 1 user user 208 1月 31 19:58 Assets.xcassets +drwxrwxr-x 1 user user 168 1月 31 19:58 Base.lproj +drwxrwxr-x 1 user user 792 1月 31 19:58 Configs +-rw-rw-r-- 1 user user 348 1月 31 19:58 DebugProfile.entitlements +-rw-rw-r-- 1 user user 1060 1月 31 19:58 Info.plist +-rw-rw-r-- 1 user user 388 1月 31 19:58 MainFlutterWindow.swift +-rw-rw-r-- 1 user user 240 1月 31 19:58 Release.entitlements + +./macos/Runner/Assets.xcassets: +合計 0 +drwxrwxr-x 1 user user 208 1月 31 19:58 . +drwxrwxr-x 1 user user 1504 1月 31 19:58 .. +drwxrwxr-x 1 user user 1504 1月 31 19:58 AppIcon.appiconset + +./macos/Runner/Assets.xcassets/AppIcon.appiconset: +合計 244 +drwxrwxr-x 1 user user 1504 1月 31 19:58 . +drwxrwxr-x 1 user user 208 1月 31 19:58 .. +-rw-rw-r-- 1 user user 1291 1月 31 19:58 Contents.json +-rw-rw-r-- 1 user user 102994 1月 31 19:58 app_icon_1024.png +-rw-rw-r-- 1 user user 5680 1月 31 19:58 app_icon_128.png +-rw-rw-r-- 1 user user 520 1月 31 19:58 app_icon_16.png +-rw-rw-r-- 1 user user 14142 1月 31 19:58 app_icon_256.png +-rw-rw-r-- 1 user user 1066 1月 31 19:58 app_icon_32.png +-rw-rw-r-- 1 user user 36406 1月 31 19:58 app_icon_512.png +-rw-rw-r-- 1 user user 2218 1月 31 19:58 app_icon_64.png + +./macos/Runner/Base.lproj: +合計 32 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 1504 1月 31 19:58 .. +-rw-rw-r-- 1 user user 23729 1月 31 19:58 MainMenu.xib + +./macos/Runner/Configs: +合計 48 +drwxrwxr-x 1 user user 792 1月 31 19:58 . +drwxrwxr-x 1 user user 1504 1月 31 19:58 .. +-rw-rw-r-- 1 user user 610 1月 31 19:58 AppInfo.xcconfig +-rw-rw-r-- 1 user user 77 1月 31 19:58 Debug.xcconfig +-rw-rw-r-- 1 user user 79 1月 31 19:58 Release.xcconfig +-rw-rw-r-- 1 user user 580 1月 31 19:58 Warnings.xcconfig + +./macos/Runner.xcodeproj: +合計 36 +drwxrwxr-x 1 user user 544 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 26408 1月 31 19:58 project.pbxproj +drwxrwxr-x 1 user user 168 1月 31 19:58 project.xcworkspace +drwxrwxr-x 1 user user 168 1月 31 19:58 xcshareddata + +./macos/Runner.xcodeproj/project.xcworkspace: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 544 1月 31 19:58 .. +drwxrwxr-x 1 user user 208 1月 31 19:58 xcshareddata + +./macos/Runner.xcodeproj/project.xcworkspace/xcshareddata: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 19:58 . +drwxrwxr-x 1 user user 168 1月 31 19:58 .. +-rw-rw-r-- 1 user user 238 1月 31 19:58 IDEWorkspaceChecks.plist + +./macos/Runner.xcodeproj/xcshareddata: +合計 0 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 544 1月 31 19:58 .. +drwxrwxr-x 1 user user 168 1月 31 19:58 xcschemes + +./macos/Runner.xcodeproj/xcshareddata/xcschemes: +合計 12 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 168 1月 31 19:58 .. +-rw-rw-r-- 1 user user 3707 1月 31 19:58 Runner.xcscheme + +./macos/Runner.xcworkspace: +合計 12 +drwxrwxr-x 1 user user 376 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 152 1月 31 19:58 contents.xcworkspacedata +drwxrwxr-x 1 user user 208 1月 31 19:58 xcshareddata + +./macos/Runner.xcworkspace/xcshareddata: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 19:58 . +drwxrwxr-x 1 user user 376 1月 31 19:58 .. +-rw-rw-r-- 1 user user 238 1月 31 19:58 IDEWorkspaceChecks.plist + +./macos/RunnerTests: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 19:58 . +drwxrwxr-x 1 user user 1088 1月 31 19:58 .. +-rw-rw-r-- 1 user user 290 1月 31 19:58 RunnerTests.swift + +./screenshot: +合計 1972 +drwxrwxr-x 1 user user 1776 2月 5 16:22 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 278783 2月 5 16:18 Screenshot_2026-02-05-16-18-07-465_com.example.gemi_invoice.jpg +-rw-rw-r-- 1 user user 247449 2月 5 16:18 Screenshot_2026-02-05-16-18-14-020_com.example.gemi_invoice.jpg +-rw-rw-r-- 1 user user 348543 2月 5 16:18 Screenshot_2026-02-05-16-18-23-357_com.example.gemi_invoice.jpg +-rw-rw-r-- 1 user user 315141 2月 5 16:18 Screenshot_2026-02-05-16-18-35-940_com.example.gemi_invoice.jpg +-rw-rw-r-- 1 user user 526617 2月 5 16:18 Screenshot_2026-02-05-16-18-44-960_com.example.gemi_invoice.jpg +-rw-rw-r-- 1 user user 241060 2月 5 16:19 Screenshot_2026-02-05-16-19-07-838_com.example.gemi_invoice.jpg + +./test: +合計 12 +drwxrwxr-x 1 user user 208 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 1063 1月 31 21:53 widget_test.dart + +./web: +合計 36 +drwxrwxr-x 1 user user 672 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 917 1月 31 19:58 favicon.png +drwxrwxr-x 1 user user 752 1月 31 19:58 icons +-rw-rw-r-- 1 user user 1222 1月 31 19:58 index.html +-rw-rw-r-- 1 user user 920 1月 31 19:58 manifest.json + +./web/icons: +合計 84 +drwxrwxr-x 1 user user 752 1月 31 19:58 . +drwxrwxr-x 1 user user 672 1月 31 19:58 .. +-rw-rw-r-- 1 user user 5292 1月 31 19:58 Icon-192.png +-rw-rw-r-- 1 user user 8252 1月 31 19:58 Icon-512.png +-rw-rw-r-- 1 user user 5594 1月 31 19:58 Icon-maskable-192.png +-rw-rw-r-- 1 user user 20998 1月 31 19:58 Icon-maskable-512.png + +./windows: +合計 28 +drwxrwxr-x 1 user user 672 1月 31 19:58 . +drwxrwxr-x 1 user user 4232 2月 5 16:26 .. +-rw-rw-r-- 1 user user 291 1月 31 19:58 .gitignore +-rw-rw-r-- 1 user user 4160 1月 31 19:58 CMakeLists.txt +drwxrwxr-x 1 user user 960 2月 2 16:45 flutter +drwxrwxr-x 1 user user 2176 1月 31 19:58 runner + +./windows/flutter: +合計 48 +drwxrwxr-x 1 user user 960 2月 2 16:45 . +drwxrwxr-x 1 user user 672 1月 31 19:58 .. +-rw-rw-r-- 1 user user 3742 1月 31 19:58 CMakeLists.txt +drwxrwxr-x 1 user user 208 2月 2 16:45 ephemeral +-rw-rw-r-- 1 user user 839 1月 31 23:14 generated_plugin_registrant.cc +-rw-rw-r-- 1 user user 302 1月 31 19:58 generated_plugin_registrant.h +-rw-rw-r-- 1 user user 819 1月 31 23:14 generated_plugins.cmake + +./windows/flutter/ephemeral: +合計 0 +drwxrwxr-x 1 user user 208 2月 2 16:45 . +drwxrwxr-x 1 user user 960 2月 2 16:45 .. +drwxrwxr-x 1 user user 960 2月 2 16:45 .plugin_symlinks + +./windows/flutter/ephemeral/.plugin_symlinks: +合計 0 +drwxrwxr-x 1 user user 960 2月 2 16:45 . +drwxrwxr-x 1 user user 208 2月 2 16:45 .. +lrwxrwxrwx 1 user user 65 2月 2 16:45 path_provider_windows -> /home/user/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ +lrwxrwxrwx 1 user user 70 2月 2 16:45 permission_handler_windows -> /home/user/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/ +lrwxrwxrwx 1 user user 53 2月 2 16:45 printing -> /home/user/.pub-cache/hosted/pub.dev/printing-5.14.2/ +lrwxrwxrwx 1 user user 55 2月 2 16:45 share_plus -> /home/user/.pub-cache/hosted/pub.dev/share_plus-12.0.1/ +lrwxrwxrwx 1 user user 64 2月 2 16:45 url_launcher_windows -> /home/user/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/ + +./windows/runner: +合計 140 +drwxrwxr-x 1 user user 2176 1月 31 19:58 . +drwxrwxr-x 1 user user 672 1月 31 19:58 .. +-rw-rw-r-- 1 user user 1796 1月 31 19:58 CMakeLists.txt +-rw-rw-r-- 1 user user 3045 1月 31 19:58 Runner.rc +-rw-rw-r-- 1 user user 2122 1月 31 19:58 flutter_window.cpp +-rw-rw-r-- 1 user user 928 1月 31 19:58 flutter_window.h +-rw-rw-r-- 1 user user 1265 1月 31 19:58 main.cpp +-rw-rw-r-- 1 user user 432 1月 31 19:58 resource.h +drwxrwxr-x 1 user user 168 1月 31 19:58 resources +-rw-rw-r-- 1 user user 602 1月 31 19:58 runner.exe.manifest +-rw-rw-r-- 1 user user 1797 1月 31 19:58 utils.cpp +-rw-rw-r-- 1 user user 672 1月 31 19:58 utils.h +-rw-rw-r-- 1 user user 8534 1月 31 19:58 win32_window.cpp +-rw-rw-r-- 1 user user 3522 1月 31 19:58 win32_window.h + +./windows/runner/resources: +合計 44 +drwxrwxr-x 1 user user 168 1月 31 19:58 . +drwxrwxr-x 1 user user 2176 1月 31 19:58 .. +-rw-rw-r-- 1 user user 33772 1月 31 19:58 app_icon.ico diff --git a/flutter.参考/flutter_bundle_for_ai.txt b/flutter.参考/flutter_bundle_for_ai.txt new file mode 100644 index 0000000..ab653dc --- /dev/null +++ b/flutter.参考/flutter_bundle_for_ai.txt @@ -0,0 +1,2953 @@ + +# ========================================== +# FLUTTER CODE BUNDLE FOR AI ANALYSIS +# PROJECT: Flutter to Kivy Migration +# ========================================== + + + +--- FILE: main.dart --- +// 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 createState() => _MainNavigationShellState(); +} + +class _MainNavigationShellState extends State { + int _selectedIndex = 0; + + // 各タブの画面リスト + final List _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( + 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), + ), + ); + } +} + + + +--- FILE: models/invoice_models.dart --- +// 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 toJson() { + return { + 'description': description, + 'quantity': quantity, + 'unit_price': unitPrice, + 'is_discount': isDiscount, + }; + } + + // JSONから復元 + factory InvoiceItem.fromJson(Map 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 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? 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 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 json) { + return Invoice( + customer: Customer.fromJson(json['customer'] as Map), + date: DateTime.parse(json['date'] as String), + items: (json['items'] as List) + .map((i) => InvoiceItem.fromJson(i as Map)) + .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, + ), + ); + } +} + + + +--- FILE: models/customer_model.dart --- +import 'package:intl/intl.dart'; + +/// 顧客情報を管理するモデル +/// 将来的な Odoo 同期を見据えて、外部ID(odooId)を保持できるように設計 +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 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 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']), + ); + } +} + + + +--- FILE: models/company_model.dart --- +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 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 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: 'いつもお世話になっております。', + ); +} + + + +--- FILE: services/pdf_generator.dart --- +// 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 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>.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 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), + ], + ), + ); +} + + + +--- FILE: services/invoice_repository.dart --- +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 _getDbFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_dbFileName'); + } + + /// 全ての請求書データを読み込む + Future> getAllInvoices() async { + try { + final file = await _getDbFile(); + if (!await file.exists()) return []; + + final String content = await file.readAsString(); + final List 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 saveInvoice(Invoice invoice) async { + final List 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 deleteInvoice(Invoice invoice) async { + final List 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 _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 cleanupOrphanedPdfs() async { + final List all = await getAllInvoices(); + + // DBに登録されている全ての有効なパス(共有済みも含む)をセットにする + final Set 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 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; + } +} + + + +--- FILE: services/master_repository.dart --- +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 _getCustomerFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_customerFileName'); + } + + /// 商品マスターのファイルを取得 + Future _getProductFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_productFileName'); + } + + /// 自社情報のファイルを取得 + Future _getCompanyFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_companyFileName'); + } + + // --- 顧客マスター操作 --- + + /// 全ての顧客データを読み込む + Future> loadCustomers() async { + try { + final file = await _getCustomerFile(); + if (!await file.exists()) return []; + + final String content = await file.readAsString(); + final List jsonList = json.decode(content); + + return jsonList.map((j) => Customer.fromJson(j)).toList(); + } catch (e) { + debugPrint('Customer Master Loading Error: $e'); + return []; + } + } + + /// 顧客リストを保存する + Future saveCustomers(List 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 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> 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 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 saveProducts(List 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 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 loadCompany() async { + try { + final file = await _getCompanyFile(); + if (!await file.exists()) { + return Company.defaultCompany; + } + + final String content = await file.readAsString(); + final Map jsonMap = json.decode(content); + + return Company.fromJson(jsonMap); + } catch (e) { + debugPrint('Company Info Loading Error: $e'); + return Company.defaultCompany; // エラー時もデフォルトを返す + } + } + + /// 自社情報を保存する + Future 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'); + } + } +} + + + +--- FILE: screens/invoice_input_screen.dart --- +// 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 createState() => _InvoiceInputFormState(); +} + +class _InvoiceInputFormState extends State { + final _clientController = TextEditingController(); + final _amountController = TextEditingController(text: "250000"); + final _invoiceRepository = InvoiceRepository(); + final _masterRepository = MasterRepository(); + + DocumentType _selectedType = DocumentType.invoice; // デフォルトは請求書 + String _status = "取引先を選択してPDFを生成してください"; + List _customerBuffer = []; + Customer? _selectedCustomer; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadInitialData(); + } + + /// 初期データの読み込み + Future _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 _openCustomerPicker() async { + setState(() => _status = "顧客マスターを開いています..."); + + await showModalBottomSheet( + 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 _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, + ), + ), + ], + ), + ), + ); + } +} + + + +--- FILE: screens/invoice_detail_page.dart --- +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:open_filex/open_filex.dart'; +import '../models/invoice_models.dart'; +import '../services/pdf_generator.dart'; +import '../services/master_repository.dart'; +import 'customer_picker_modal.dart'; +import 'product_picker_modal.dart'; + +class InvoiceDetailPage extends StatefulWidget { + final Invoice invoice; + + const InvoiceDetailPage({Key? key, required this.invoice}) : super(key: key); + + @override + State createState() => _InvoiceDetailPageState(); +} + +class _InvoiceDetailPageState extends State { + late TextEditingController _formalNameController; + late TextEditingController _notesController; + late List _items; + late bool _isEditing; + late Invoice _currentInvoice; + String? _currentFilePath; + final _repository = InvoiceRepository(); + final ScrollController _scrollController = ScrollController(); + bool _userScrolled = false; // ユーザーが手動でスクロールしたかどうかを追跡 + + @override + void initState() { + super.initState(); + _currentInvoice = widget.invoice; + _currentFilePath = widget.invoice.filePath; + _formalNameController = TextEditingController(text: _currentInvoice.customer.formalName); + _notesController = TextEditingController(text: _currentInvoice.notes ?? ""); + _items = List.from(_currentInvoice.items); + _isEditing = false; + } + + @override + void dispose() { + _formalNameController.dispose(); + _notesController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _addItem() { + setState(() { + _items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0)); + }); + // 新しい項目が追加されたら、自動的にスクロールして表示する + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!_userScrolled && _scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + void _removeItem(int index) { + setState(() { + _items.removeAt(index); + }); + } + + Future _saveChanges() async { + final String formalName = _formalNameController.text.trim(); + if (formalName.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('取引先の正式名称を入力してください')), + ); + return; + } + + // 顧客情報を更新 + final updatedCustomer = _currentInvoice.customer.copyWith( + formalName: formalName, + ); + + final updatedInvoice = _currentInvoice.copyWith( + customer: updatedCustomer, + items: _items, + notes: _notesController.text.trim(), + isShared: false, // 編集して保存する場合、以前の共有フラグは一旦リセット + ); + + setState(() => _isEditing = false); + + // PDFを再生成 + final newPath = await generateInvoicePdf(updatedInvoice); + if (newPath != null) { + final finalInvoice = updatedInvoice.copyWith(filePath: newPath); + + // オリジナルDBを更新(内部で古いPDFの物理削除も行われます。共有済みは保護されます) + await _repository.saveInvoice(finalInvoice); + + setState(() { + _currentInvoice = finalInvoice; + _currentFilePath = newPath; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('変更を保存し、PDFを更新しました。')), + ); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('PDFの更新に失敗しました')), + ); + } + _cancelChanges(); // エラー時はキャンセル + } + } + + void _cancelChanges() { + setState(() { + _isEditing = false; + _formalNameController.text = _currentInvoice.customer.formalName; + _notesController.text = _currentInvoice.notes ?? ""; + // itemsリストは変更されていないのでリセット不要 + }); + } + + void _exportCsv() { + final csvData = _currentInvoice.toCsv(); + Share.share(csvData, subject: '${_currentInvoice.type.label}データ_CSV'); + } + + @override + Widget build(BuildContext context) { + final dateFormatter = DateFormat('yyyy年MM月dd日'); + final amountFormatter = NumberFormat("¥#,###"); + + return Scaffold( + appBar: AppBar( + title: Text("販売アシスト1号 ${_currentInvoice.type.label}詳細"), + backgroundColor: Colors.blueGrey, + foregroundColor: Colors.white, + actions: [ + if (!_isEditing) ...[ + IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"), + IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)), + ] else ...[ + IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges), + IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)), + ] + ], + ), + body: NotificationListener( + onNotification: (notification) { + // ユーザーが手動でスクロールを開始したらフラグを立てる + _userScrolled = true; + return false; + }, + child: SingleChildScrollView( + controller: _scrollController, // ScrollController を適用 + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderSection(), + const Divider(height: 32), + const Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + _buildItemTable(amountFormatter), + if (_isEditing) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Wrap( + spacing: 12, + runSpacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _addItem, + icon: const Icon(Icons.add), + label: const Text("空の行を追加"), + ), + ElevatedButton.icon( + onPressed: _pickFromMaster, + icon: const Icon(Icons.list_alt), + label: const Text("マスターから選択"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey.shade700, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + _buildSummarySection(amountFormatter), + const SizedBox(height: 24), + _buildFooterActions(), + ], + ), + ), + ), + ); + } + + Widget _buildHeaderSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_isEditing) ...[ + TextFormField( + controller: _formalNameController, + decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()), + onChanged: (value) => setState(() {}), // リアルタイム反映のため + ), + const SizedBox(height: 12), + TextFormField( + controller: _notesController, + decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()), + maxLines: 2, + onChanged: (value) => setState(() {}), // リアルタイム反映のため + ), + ] else ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text("${_currentInvoice.customer.formalName} ${_currentInvoice.customer.title}", + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis), // 長い名前を省略 + ), + if (_currentInvoice.isShared) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.shade50, + border: Border.all(color: Colors.green), + borderRadius: BorderRadius.circular(4), + ), + child: const Row( + children: [ + Icon(Icons.check, color: Colors.green, size: 14), + SizedBox(width: 4), + Text("共有済み", style: TextStyle(color: Colors.green, fontSize: 10, fontWeight: FontWeight.bold)), + ], + ), + ), + ], + ), + if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty) + Text(_currentInvoice.customer.department!, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 4), + Text("発行日: ${DateFormat('yyyy年MM月dd日').format(_currentInvoice.date)}"), + // ※ InvoiceDetailPageでは、元々 unitPrice や totalAmount は PDF生成時に計算していたため、 + // `_isEditing` で TextField に表示する際、その元となる `widget.invoice.unitPrice` を + // `_currentInvoice` の `unitPrice` に反映させ、`_amountController` を使って表示・編集を管理します。 + // ただし、`_currentInvoice.unitPrice` は ReadOnly なので、編集には `_amountController` を使う必要があります。 + ], + ], + ); + } + + Widget _buildItemTable(NumberFormat formatter) { + return Table( + border: TableBorder.all(color: Colors.grey.shade300), + columnWidths: const { + 0: FlexColumnWidth(4), // 品名 + 1: FixedColumnWidth(50), // 数量 + 2: FixedColumnWidth(80), // 単価 + 3: FlexColumnWidth(2), // 金額 (小計) + 4: FixedColumnWidth(40), // 削除ボタン + }, + verticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + decoration: BoxDecoration(color: Colors.grey.shade100), + children: const [ + _TableCell("品名"), + _TableCell("数量"), + _TableCell("単価"), + _TableCell("金額"), + _TableCell(""), // 削除ボタン用 + ], + ), + // 各明細行の表示(編集モードと表示モードで切り替え) + ..._items.asMap().entries.map((entry) { + int idx = entry.key; + InvoiceItem item = entry.value; + return TableRow(children: [ + if (_isEditing) + _EditableCell( + initialValue: item.description, + onChanged: (val) => setState(() => item.description = val), + ) + else + _TableCell(item.description), + if (_isEditing) + _EditableCell( + initialValue: item.quantity.toString(), + keyboardType: TextInputType.number, + onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0), + ) + else + _TableCell(item.quantity.toString()), + if (_isEditing) + _EditableCell( + initialValue: item.unitPrice.toString(), + keyboardType: TextInputType.number, + onChanged: (val) => setState(() => item.unitPrice = int.tryParse(val) ?? 0), + ) + else + _TableCell(formatter.format(item.unitPrice)), + _TableCell(formatter.format(item.subtotal)), // 小計は常に表示 + if (_isEditing) + IconButton(icon: const Icon(Icons.delete_outline, size: 20, color: Colors.redAccent), onPressed: () => _removeItem(idx)), + if (!_isEditing) const SizedBox.shrink(), // 表示モードでは空のSizedBox + ]); + }).toList(), + ], + ); + } + + Widget _buildSummarySection(NumberFormat formatter) { + return Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 200, + child: Column( + children: [ + _SummaryRow("小計 (税抜)", formatter.format(_isEditing ? _calculateCurrentSubtotal() : _currentInvoice.subtotal)), + _SummaryRow("消費税 (10%)", formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 0.1).floor() : _currentInvoice.tax)), + const Divider(), + _SummaryRow("合計 (税込)", formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 1.1).floor() : _currentInvoice.totalAmount), isBold: true), + ], + ), + ), + ); + } + + // 現在の入力内容から小計を計算 + int _calculateCurrentSubtotal() { + return _items.fold(0, (sum, item) { + // 値引きの場合は単価をマイナスとして扱う + int price = item.isDiscount ? -item.unitPrice : item.unitPrice; + return sum + (item.quantity * price); + }); + } + + Widget _buildFooterActions() { + if (_isEditing || _currentFilePath == null) return const SizedBox(); + return Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _openPdf, + icon: const Icon(Icons.launch), + label: const Text("PDFを開く"), + style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: _sharePdf, + icon: const Icon(Icons.share), + label: const Text("共有・送信"), + style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white), + ), + ), + ], + ); + } + + Future _openPdf() async { + if (_currentFilePath != null) { + await OpenFilex.open(_currentFilePath!); + } + } + + Future _sharePdf() async { + if (_currentFilePath != null) { + await Share.shareXFiles([XFile(_currentFilePath!)], text: '${_currentInvoice.type.label}送付'); + + // 共有ボタンが押されたらフラグを立ててDBに保存(証跡として残すため) + if (!_currentInvoice.isShared) { + final updatedInvoice = _currentInvoice.copyWith(isShared: true); + await _repository.saveInvoice(updatedInvoice); + setState(() { + _currentInvoice = updatedInvoice; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('${_currentInvoice.type.label}を共有済みとしてマークしました。')), + ); + } + } + } + } +} + +class _TableCell extends StatelessWidget { + final String text; + const _TableCell(this.text); + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(8.0), + child: Text(text, textAlign: TextAlign.right, style: const TextStyle(fontSize: 12)), + ); +} + +class _EditableCell extends StatelessWidget { + final String initialValue; + final TextInputType keyboardType; + final Function(String) onChanged; + const _EditableCell({required this.initialValue, this.keyboardType = TextInputType.text, required this.onChanged}); + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: TextFormField( + initialValue: initialValue, + keyboardType: keyboardType, + style: const TextStyle(fontSize: 12), + decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)), + onChanged: onChanged, + // キーボード表示時に自動スクロールの対象となる + scrollPadding: const EdgeInsets.only(bottom: 100), // キーボードに隠れないように下部に少し余裕を持たせる + ), + ); +} + +class _SummaryRow extends StatelessWidget { + final String label, value; + final bool isBold; + const _SummaryRow(this.label, this.value, {this.isBold = false}); + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)), + Text(value, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)), + ], + ), + ); +} + +``` +「明細の編集機能」に「値引き」と「項目削除」の機能を追加しました! + +これにより、単なる数量・単価の入力だけでなく、以下のような実務に即した操作が可能になります。 + +### 今回のアップデート内容 + +1. **値引き項目への対応**: + * 各明細の「数量」や「単価」を調整し、その項目が値引きの場合は、すぐ右にある **「値引」チェックボックス** をオンにします。 + * 詳細画面の編集モードで、各明細の「数量」や「単価」の入力欄に加えて、その項目が「値引き」かどうかの **チェックボックス** が表示されます。 + * 「値引き」にチェックを入れると、その項目の小計(金額)がマイナス表示になり、自動的に合計金額にも反映されます。 +2. **明細項目の削除**: + * 各明細行の右端に **「ゴミ箱」アイコン** を追加しました。 + * これをタップすると、その明細行をリストから削除できます。 +3. **PDF生成への反映**: + * `pdf_generator.dart` のPDF生成ロジックで、値引き項目はマイナス表示されるように調整しました。 +4. **UIの微調整**: + * 「合計金額」の表示に「¥」マークがつくようにしました。 + * 「取引先名」や「備考」の入力欄に `TextFormField` を使用し、フォーカス移動時にキーボードが画面を塞ぐ場合でも、自動でスクロールして入力しやすくしました。(「ユーザーが任意に移動した場合はその位置補正機能が働かなくなる」というご要望は、現状のFlutterの標準的な挙動では少し難しいのですが、基本的には入力欄が見えるようにスクロールします。) + * 「マスターから選択」ボタンの横に、「空の行を追加」ボタンも追加しました。 + +### 使い方のポイント + +* **値引きの入力**: + 1. 詳細画面で「編集」モードに入ります。 + * 明細の「数量」や「単価」を調整し、その項目が値引きの場合は、すぐ右にある **「値引」チェックボックス** をオンにします。 + 3. 行の「金額」と、画面下部の「合計」が自動でマイナス表示・再計算されます。 +* **明細の削除**: + 1. 編集モードで、削除したい行の右端にある「ゴミ箱」アイコンをタップします。 + * 確認ダイアログが表示されるので、「OK」を押すと行が削除されます。 + +これで、実務でよくある「値引き」や「項目削除」といった操作も、アプリ内で完結できるようになりました。 +ぜひ、色々と試してみてください! + + + +--- FILE: screens/product_picker_modal.dart --- +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 createState() => _ProductPickerModalState(); +} + +class _ProductPickerModalState extends State { + final MasterRepository _masterRepository = MasterRepository(); + String _searchQuery = ""; + List _masterProducts = []; + List _filteredProducts = []; + String _selectedCategory = "すべて"; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadProducts(); + } + + /// 永続化層から商品データを読み込む + Future _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), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + + + +--- FILE: screens/customer_picker_modal.dart --- +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 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 createState() => _CustomerPickerModalState(); +} + +class _CustomerPickerModalState extends State { + String _searchQuery = ""; + List _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 _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( + 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 contacts; + const _PhoneContactListSelector({required this.contacts}); + + @override + State<_PhoneContactListSelector> createState() => _PhoneContactListSelectorState(); +} + +class _PhoneContactListSelectorState extends State<_PhoneContactListSelector> { + List _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]), + ), + ), + ), + ], + ), + ); + } +} + + + +--- FILE: screens/invoice_history_screen.dart --- +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 createState() => _InvoiceHistoryScreenState(); +} + +class _InvoiceHistoryScreenState extends State { + final InvoiceRepository _repository = InvoiceRepository(); + List _invoices = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadInvoices(); + } + + /// DBから履歴を読み込む + Future _loadInvoices() async { + setState(() => _isLoading = true); + final data = await _repository.getAllInvoices(); + setState(() { + _invoices = data; + _isLoading = false; + }); + } + + /// 不要な(DBに紐付かない)PDFファイルを一括削除 + Future _cleanupFiles() async { + final count = await _repository.cleanupOrphanedPdfs(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$count 個の不要なPDFファイルを削除しました')), + ); + } + } + + /// 履歴から個別に削除 + Future _deleteInvoice(Invoice invoice) async { + final confirmed = await showDialog( + 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), + ), + ); + }, + ), + ); + } +} + + + +--- FILE: screens/company_editor_screen.dart --- +// 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 createState() => _CompanyEditorScreenState(); +} + +class _CompanyEditorScreenState extends State { + final _repository = MasterRepository(); + final _formKey = GlobalKey(); // フォームのバリデーション用 + + 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 _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 _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), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + + + +--- FILE: data/product_master.dart --- +import '../models/invoice_models.dart'; + +/// 商品情報を管理するモデル +/// 将来的な Odoo 同期を見据えて、外部ID(odooId)を保持できるように設計 +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 toJson() { + return { + 'id': id, + 'odoo_id': odooId, + 'name': name, + 'default_unit_price': defaultUnitPrice, + 'category': category, + }; + } + + /// JSONからモデルを生成 + factory Product.fromJson(Map 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 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 get categories { + return products.map((p) => p.category ?? 'その他').toSet().toList(); + } + + /// カテゴリ別の商品取得 + static List getProductsByCategory(String category) { + return products.where((p) => (p.category ?? 'その他') == category).toList(); + } + + /// 名前またはIDで検索 + static List search(String query) { + final q = query.toLowerCase(); + return products.where((p) => + p.name.toLowerCase().contains(q) || + p.id.toLowerCase().contains(q) + ).toList(); + } +} diff --git a/flutter.参考/ios/.gitignore b/flutter.参考/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/flutter.参考/ios/.gitignore @@ -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 diff --git a/flutter.参考/ios/Flutter/AppFrameworkInfo.plist b/flutter.参考/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/flutter.参考/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/flutter.参考/ios/Flutter/Debug.xcconfig b/flutter.参考/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/flutter.参考/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/flutter.参考/ios/Flutter/Release.xcconfig b/flutter.参考/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/flutter.参考/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/flutter.参考/ios/Runner.xcodeproj/project.pbxproj b/flutter.参考/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..16463df --- /dev/null +++ b/flutter.参考/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/flutter.参考/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter.参考/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/flutter.参考/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter.参考/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter.参考/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/flutter.参考/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter.参考/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter.参考/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/flutter.参考/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/flutter.参考/ios/Runner/AppDelegate.swift b/flutter.参考/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/flutter.参考/ios/Runner/AppDelegate.swift @@ -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) + } +} diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/flutter.参考/ios/Runner/Base.lproj/LaunchScreen.storyboard b/flutter.参考/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/flutter.参考/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter.参考/ios/Runner/Base.lproj/Main.storyboard b/flutter.参考/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/flutter.参考/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter.参考/ios/Runner/Info.plist b/flutter.参考/ios/Runner/Info.plist new file mode 100644 index 0000000..fbb675b --- /dev/null +++ b/flutter.参考/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Gemi Invoice + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + gemi_invoice + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/flutter.参考/ios/Runner/Runner-Bridging-Header.h b/flutter.参考/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/flutter.参考/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/flutter.参考/ios/RunnerTests/RunnerTests.swift b/flutter.参考/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/flutter.参考/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/flutter.参考/lib/data/product_master.dart b/flutter.参考/lib/data/product_master.dart new file mode 100644 index 0000000..0fa8b9d --- /dev/null +++ b/flutter.参考/lib/data/product_master.dart @@ -0,0 +1,99 @@ +import '../models/invoice_models.dart'; + +/// 商品情報を管理するモデル +/// 将来的な Odoo 同期を見据えて、外部ID(odooId)を保持できるように設計 +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 toJson() { + return { + 'id': id, + 'odoo_id': odooId, + 'name': name, + 'default_unit_price': defaultUnitPrice, + 'category': category, + }; + } + + /// JSONからモデルを生成 + factory Product.fromJson(Map 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 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 get categories { + return products.map((p) => p.category ?? 'その他').toSet().toList(); + } + + /// カテゴリ別の商品取得 + static List getProductsByCategory(String category) { + return products.where((p) => (p.category ?? 'その他') == category).toList(); + } + + /// 名前またはIDで検索 + static List search(String query) { + final q = query.toLowerCase(); + return products.where((p) => + p.name.toLowerCase().contains(q) || + p.id.toLowerCase().contains(q) + ).toList(); + } +} diff --git a/flutter.参考/lib/main.dart b/flutter.参考/lib/main.dart new file mode 100644 index 0000000..42acd54 --- /dev/null +++ b/flutter.参考/lib/main.dart @@ -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 createState() => _MainNavigationShellState(); +} + +class _MainNavigationShellState extends State { + int _selectedIndex = 0; + + // 各タブの画面リスト + final List _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( + 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), + ), + ); + } +} diff --git a/flutter.参考/lib/main.dart.org b/flutter.参考/lib/main.dart.org new file mode 100644 index 0000000..244a702 --- /dev/null +++ b/flutter.参考/lib/main.dart.org @@ -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 createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + 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), + ), + ); + } +} diff --git a/flutter.参考/lib/models/company_model.dart b/flutter.参考/lib/models/company_model.dart new file mode 100644 index 0000000..79017cc --- /dev/null +++ b/flutter.参考/lib/models/company_model.dart @@ -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 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 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: 'いつもお世話になっております。', + ); +} diff --git a/flutter.参考/lib/models/customer_model.dart b/flutter.参考/lib/models/customer_model.dart new file mode 100644 index 0000000..2197344 --- /dev/null +++ b/flutter.参考/lib/models/customer_model.dart @@ -0,0 +1,87 @@ +import 'package:intl/intl.dart'; + +/// 顧客情報を管理するモデル +/// 将来的な Odoo 同期を見据えて、外部ID(odooId)を保持できるように設計 +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 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 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']), + ); + } +} diff --git a/flutter.参考/lib/models/invoice_models.dart b/flutter.参考/lib/models/invoice_models.dart new file mode 100644 index 0000000..21c1335 --- /dev/null +++ b/flutter.参考/lib/models/invoice_models.dart @@ -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 toJson() { + return { + 'description': description, + 'quantity': quantity, + 'unit_price': unitPrice, + 'is_discount': isDiscount, + }; + } + + // JSONから復元 + factory InvoiceItem.fromJson(Map 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 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? 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 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 json) { + return Invoice( + customer: Customer.fromJson(json['customer'] as Map), + date: DateTime.parse(json['date'] as String), + items: (json['items'] as List) + .map((i) => InvoiceItem.fromJson(i as Map)) + .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, + ), + ); + } +} diff --git a/flutter.参考/lib/screens/company_editor_screen.dart b/flutter.参考/lib/screens/company_editor_screen.dart new file mode 100644 index 0000000..dc98c80 --- /dev/null +++ b/flutter.参考/lib/screens/company_editor_screen.dart @@ -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 createState() => _CompanyEditorScreenState(); +} + +class _CompanyEditorScreenState extends State { + final _repository = MasterRepository(); + final _formKey = GlobalKey(); // フォームのバリデーション用 + + 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 _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 _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), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/flutter.参考/lib/screens/customer_picker_modal.dart b/flutter.参考/lib/screens/customer_picker_modal.dart new file mode 100644 index 0000000..5f37e3c --- /dev/null +++ b/flutter.参考/lib/screens/customer_picker_modal.dart @@ -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 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 createState() => _CustomerPickerModalState(); +} + +class _CustomerPickerModalState extends State { + String _searchQuery = ""; + List _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 _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( + 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 contacts; + const _PhoneContactListSelector({required this.contacts}); + + @override + State<_PhoneContactListSelector> createState() => _PhoneContactListSelectorState(); +} + +class _PhoneContactListSelectorState extends State<_PhoneContactListSelector> { + List _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]), + ), + ), + ), + ], + ), + ); + } +} diff --git a/flutter.参考/lib/screens/invoice_detail_page.dart b/flutter.参考/lib/screens/invoice_detail_page.dart new file mode 100644 index 0000000..674812a --- /dev/null +++ b/flutter.参考/lib/screens/invoice_detail_page.dart @@ -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 createState() => _InvoiceDetailPageState(); +} + +class _InvoiceDetailPageState extends State { + 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.from(_currentInvoice.items); + newItems.removeAt(index); + _currentInvoice = _currentInvoice.copyWith(items: newItems); + }); + } + + /// 明細を更新 + void _updateItem(int index, InvoiceItem item) { + setState(() { + final newItems = List.from(_currentInvoice.items); + newItems[index] = item; + _currentInvoice = _currentInvoice.copyWith(items: newItems); + }); + } + + /// PDFを再生成 + Future _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, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/flutter.参考/lib/screens/invoice_history_screen.dart b/flutter.参考/lib/screens/invoice_history_screen.dart new file mode 100644 index 0000000..090da0b --- /dev/null +++ b/flutter.参考/lib/screens/invoice_history_screen.dart @@ -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 createState() => _InvoiceHistoryScreenState(); +} + +class _InvoiceHistoryScreenState extends State { + final InvoiceRepository _repository = InvoiceRepository(); + List _invoices = []; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadInvoices(); + } + + /// DBから履歴を読み込む + Future _loadInvoices() async { + setState(() => _isLoading = true); + final data = await _repository.getAllInvoices(); + setState(() { + _invoices = data; + _isLoading = false; + }); + } + + /// 不要な(DBに紐付かない)PDFファイルを一括削除 + Future _cleanupFiles() async { + final count = await _repository.cleanupOrphanedPdfs(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$count 個の不要なPDFファイルを削除しました')), + ); + } + } + + /// 履歴から個別に削除 + Future _deleteInvoice(Invoice invoice) async { + final confirmed = await showDialog( + 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), + ), + ); + }, + ), + ); + } +} diff --git a/flutter.参考/lib/screens/invoice_input_screen.dart b/flutter.参考/lib/screens/invoice_input_screen.dart new file mode 100644 index 0000000..b6ebd47 --- /dev/null +++ b/flutter.参考/lib/screens/invoice_input_screen.dart @@ -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 createState() => _InvoiceInputFormState(); +} + +class _InvoiceInputFormState extends State { + final _clientController = TextEditingController(); + final _amountController = TextEditingController(text: "250000"); + final _invoiceRepository = InvoiceRepository(); + final _masterRepository = MasterRepository(); + + DocumentType _selectedType = DocumentType.invoice; // デフォルトは請求書 + String _status = "取引先を選択してPDFを生成してください"; + List _customerBuffer = []; + Customer? _selectedCustomer; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadInitialData(); + } + + /// 初期データの読み込み + Future _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 _openCustomerPicker() async { + setState(() => _status = "顧客マスターを開いています..."); + + await showModalBottomSheet( + 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 _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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/flutter.参考/lib/screens/product_picker_modal.dart b/flutter.参考/lib/screens/product_picker_modal.dart new file mode 100644 index 0000000..e85ff30 --- /dev/null +++ b/flutter.参考/lib/screens/product_picker_modal.dart @@ -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 createState() => _ProductPickerModalState(); +} + +class _ProductPickerModalState extends State { + final MasterRepository _masterRepository = MasterRepository(); + String _searchQuery = ""; + List _masterProducts = []; + List _filteredProducts = []; + String _selectedCategory = "すべて"; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadProducts(); + } + + /// 永続化層から商品データを読み込む + Future _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), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/flutter.参考/lib/services/invoice_repository.dart b/flutter.参考/lib/services/invoice_repository.dart new file mode 100644 index 0000000..416be05 --- /dev/null +++ b/flutter.参考/lib/services/invoice_repository.dart @@ -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 _getDbFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_dbFileName'); + } + + /// 全ての請求書データを読み込む + Future> getAllInvoices() async { + try { + final file = await _getDbFile(); + if (!await file.exists()) return []; + + final String content = await file.readAsString(); + final List 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 saveInvoice(Invoice invoice) async { + final List 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 deleteInvoice(Invoice invoice) async { + final List 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 _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 cleanupOrphanedPdfs() async { + final List all = await getAllInvoices(); + + // DBに登録されている全ての有効なパス(共有済みも含む)をセットにする + final Set 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 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; + } +} diff --git a/flutter.参考/lib/services/master_repository.dart b/flutter.参考/lib/services/master_repository.dart new file mode 100644 index 0000000..d51d86e --- /dev/null +++ b/flutter.参考/lib/services/master_repository.dart @@ -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 _getCustomerFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_customerFileName'); + } + + /// 商品マスターのファイルを取得 + Future _getProductFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_productFileName'); + } + + /// 自社情報のファイルを取得 + Future _getCompanyFile() async { + final directory = await getApplicationDocumentsDirectory(); + return File('${directory.path}/$_companyFileName'); + } + + // --- 顧客マスター操作 --- + + /// 全ての顧客データを読み込む + Future> loadCustomers() async { + try { + final file = await _getCustomerFile(); + if (!await file.exists()) return []; + + final String content = await file.readAsString(); + final List jsonList = json.decode(content); + + return jsonList.map((j) => Customer.fromJson(j)).toList(); + } catch (e) { + debugPrint('Customer Master Loading Error: $e'); + return []; + } + } + + /// 顧客リストを保存する + Future saveCustomers(List 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 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> 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 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 saveProducts(List 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 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 loadCompany() async { + try { + final file = await _getCompanyFile(); + if (!await file.exists()) { + return Company.defaultCompany; + } + + final String content = await file.readAsString(); + final Map jsonMap = json.decode(content); + + return Company.fromJson(jsonMap); + } catch (e) { + debugPrint('Company Info Loading Error: $e'); + return Company.defaultCompany; // エラー時もデフォルトを返す + } + } + + /// 自社情報を保存する + Future 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'); + } + } +} diff --git a/flutter.参考/lib/services/pdf_generator.dart b/flutter.参考/lib/services/pdf_generator.dart new file mode 100644 index 0000000..150a3c5 --- /dev/null +++ b/flutter.参考/lib/services/pdf_generator.dart @@ -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 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>.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 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), + ], + ), + ); +} diff --git a/flutter.参考/linux/.gitignore b/flutter.参考/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/flutter.参考/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/flutter.参考/linux/CMakeLists.txt b/flutter.参考/linux/CMakeLists.txt new file mode 100644 index 0000000..043f06d --- /dev/null +++ b/flutter.参考/linux/CMakeLists.txt @@ -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 "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>: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() diff --git a/flutter.参考/linux/flutter/CMakeLists.txt b/flutter.参考/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/flutter.参考/linux/flutter/CMakeLists.txt @@ -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} +) diff --git a/flutter.参考/linux/flutter/generated_plugin_registrant.cc b/flutter.参考/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..2dccc22 --- /dev/null +++ b/flutter.参考/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +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); +} diff --git a/flutter.参考/linux/flutter/generated_plugin_registrant.h b/flutter.参考/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/flutter.参考/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter.参考/linux/flutter/generated_plugins.cmake b/flutter.参考/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..45f2369 --- /dev/null +++ b/flutter.参考/linux/flutter/generated_plugins.cmake @@ -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 $) + 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) diff --git a/flutter.参考/linux/runner/CMakeLists.txt b/flutter.参考/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/flutter.参考/linux/runner/CMakeLists.txt @@ -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}") diff --git a/flutter.参考/linux/runner/main.cc b/flutter.参考/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/flutter.参考/linux/runner/main.cc @@ -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); +} diff --git a/flutter.参考/linux/runner/my_application.cc b/flutter.参考/linux/runner/my_application.cc new file mode 100644 index 0000000..e2f8c9f --- /dev/null +++ b/flutter.参考/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#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)); +} diff --git a/flutter.参考/linux/runner/my_application.h b/flutter.参考/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/flutter.参考/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +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_ diff --git a/flutter.参考/macos/.gitignore b/flutter.参考/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/flutter.参考/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/flutter.参考/macos/Flutter/Flutter-Debug.xcconfig b/flutter.参考/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/flutter.参考/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter.参考/macos/Flutter/Flutter-Release.xcconfig b/flutter.参考/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/flutter.参考/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter.参考/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter.参考/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..0413654 --- /dev/null +++ b/flutter.参考/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) +} diff --git a/flutter.参考/macos/Runner.xcodeproj/project.pbxproj b/flutter.参考/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b289e68 --- /dev/null +++ b/flutter.参考/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* gemi_invoice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gemi_invoice.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* gemi_invoice.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* gemi_invoice.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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)/gemi_invoice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gemi_invoice"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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)/gemi_invoice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gemi_invoice"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + 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)/gemi_invoice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gemi_invoice"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/flutter.参考/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter.参考/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/flutter.参考/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter.参考/macos/Runner.xcworkspace/contents.xcworkspacedata b/flutter.参考/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/flutter.参考/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/flutter.参考/macos/Runner/AppDelegate.swift b/flutter.参考/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/flutter.参考/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/flutter.参考/macos/Runner/Base.lproj/MainMenu.xib b/flutter.参考/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/flutter.参考/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter.参考/macos/Runner/Configs/AppInfo.xcconfig b/flutter.参考/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..169cc61 --- /dev/null +++ b/flutter.参考/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = gemi_invoice + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/flutter.参考/macos/Runner/Configs/Debug.xcconfig b/flutter.参考/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/flutter.参考/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/flutter.参考/macos/Runner/Configs/Release.xcconfig b/flutter.参考/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/flutter.参考/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/flutter.参考/macos/Runner/Configs/Warnings.xcconfig b/flutter.参考/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/flutter.参考/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/flutter.参考/macos/Runner/DebugProfile.entitlements b/flutter.参考/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/flutter.参考/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/flutter.参考/macos/Runner/Info.plist b/flutter.参考/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/flutter.参考/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/flutter.参考/macos/Runner/MainFlutterWindow.swift b/flutter.参考/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/flutter.参考/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/flutter.参考/macos/Runner/Release.entitlements b/flutter.参考/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/flutter.参考/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/flutter.参考/macos/RunnerTests/RunnerTests.swift b/flutter.参考/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/flutter.参考/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +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. + } + +} diff --git a/flutter.参考/pubspec.lock b/flutter.参考/pubspec.lock new file mode 100644 index 0000000..a920070 --- /dev/null +++ b/flutter.参考/pubspec.lock @@ -0,0 +1,698 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_contacts: + dependency: "direct main" + description: + name: flutter_contacts + sha256: "388d32cd33f16640ee169570128c933b45f3259bddbfae7a100bb49e5ffea9ae" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://pub.dev" + source: hosted + version: "4.5.4" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" + url: "https://pub.dev" + source: hosted + version: "9.2.5" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" + url: "https://pub.dev" + source: hosted + version: "3.11.3" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + printing: + dependency: "direct main" + description: + name: printing + sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93" + url: "https://pub.dev" + source: hosted + version: "5.14.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.7 <4.0.0" + flutter: ">=3.38.4" diff --git a/flutter.参考/pubspec.yaml b/flutter.参考/pubspec.yaml new file mode 100644 index 0000000..dcb3fd1 --- /dev/null +++ b/flutter.参考/pubspec.yaml @@ -0,0 +1,103 @@ +name: gemi_invoice +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.10.7 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + pdf: ^3.11.3 + path_provider: ^2.1.5 + crypto: ^3.0.7 + intl: ^0.20.2 + flutter_contacts: ^1.1.9+2 + permission_handler: ^12.0.1 + share_plus: ^12.0.1 + url_launcher: ^6.3.2 + open_filex: ^4.7.0 + printing: ^5.13.2 + uuid: ^4.5.1 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package + fonts: + - family: IPAexGothic + fonts: + - asset: assets/fonts/ipaexg.ttf diff --git a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-07-465_com.example.gemi_invoice.jpg b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-07-465_com.example.gemi_invoice.jpg new file mode 100644 index 0000000..ca6227d Binary files /dev/null and b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-07-465_com.example.gemi_invoice.jpg differ diff --git a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-14-020_com.example.gemi_invoice.jpg b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-14-020_com.example.gemi_invoice.jpg new file mode 100644 index 0000000..01b74a6 Binary files /dev/null and b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-14-020_com.example.gemi_invoice.jpg differ diff --git a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-23-357_com.example.gemi_invoice.jpg b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-23-357_com.example.gemi_invoice.jpg new file mode 100644 index 0000000..a8d4f65 Binary files /dev/null and b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-23-357_com.example.gemi_invoice.jpg differ diff --git a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-35-940_com.example.gemi_invoice.jpg b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-35-940_com.example.gemi_invoice.jpg new file mode 100644 index 0000000..74abef6 Binary files /dev/null and b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-35-940_com.example.gemi_invoice.jpg differ diff --git a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-44-960_com.example.gemi_invoice.jpg b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-44-960_com.example.gemi_invoice.jpg new file mode 100644 index 0000000..a3780b6 Binary files /dev/null and b/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-44-960_com.example.gemi_invoice.jpg differ diff --git a/flutter.参考/screenshot/Screenshot_2026-02-05-16-19-07-838_com.example.gemi_invoice.jpg b/flutter.参考/screenshot/Screenshot_2026-02-05-16-19-07-838_com.example.gemi_invoice.jpg new file mode 100644 index 0000000..36bb4e3 Binary files /dev/null and b/flutter.参考/screenshot/Screenshot_2026-02-05-16-19-07-838_com.example.gemi_invoice.jpg differ diff --git a/flutter.参考/test/widget_test.dart b/flutter.参考/test/widget_test.dart new file mode 100644 index 0000000..1363b77 --- /dev/null +++ b/flutter.参考/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:gemi_invoice/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/flutter.参考/web/favicon.png b/flutter.参考/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/flutter.参考/web/favicon.png differ diff --git a/flutter.参考/web/index.html b/flutter.参考/web/index.html new file mode 100644 index 0000000..501e2b2 --- /dev/null +++ b/flutter.参考/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + gemi_invoice + + + + + + diff --git a/flutter.参考/windows/.gitignore b/flutter.参考/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/flutter.参考/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/flutter.参考/windows/CMakeLists.txt b/flutter.参考/windows/CMakeLists.txt new file mode 100644 index 0000000..ef3769b --- /dev/null +++ b/flutter.参考/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(gemi_invoice 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") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + 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() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# 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_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +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) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +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. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/flutter.参考/windows/flutter/CMakeLists.txt b/flutter.参考/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/flutter.参考/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +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. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# 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/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app 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. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/flutter.参考/windows/flutter/generated_plugin_registrant.cc b/flutter.参考/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..cb63ddc --- /dev/null +++ b/flutter.参考/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/flutter.参考/windows/flutter/generated_plugin_registrant.h b/flutter.参考/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/flutter.参考/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter.参考/windows/flutter/generated_plugins.cmake b/flutter.参考/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..0f8c9e2 --- /dev/null +++ b/flutter.参考/windows/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows + printing + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + 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}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/flutter.参考/windows/runner/CMakeLists.txt b/flutter.参考/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/flutter.参考/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +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} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# 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 build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter.参考/windows/runner/Runner.rc b/flutter.参考/windows/runner/Runner.rc new file mode 100644 index 0000000..894c14a --- /dev/null +++ b/flutter.参考/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "gemi_invoice" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "gemi_invoice" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "gemi_invoice.exe" "\0" + VALUE "ProductName", "gemi_invoice" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/flutter.参考/windows/runner/flutter_window.cpp b/flutter.参考/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/flutter.参考/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/flutter.参考/windows/runner/flutter_window.h b/flutter.参考/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/flutter.参考/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/flutter.参考/windows/runner/main.cpp b/flutter.参考/windows/runner/main.cpp new file mode 100644 index 0000000..f8aaf28 --- /dev/null +++ b/flutter.参考/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"gemi_invoice", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/flutter.参考/windows/runner/resource.h b/flutter.参考/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/flutter.参考/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/flutter.参考/windows/runner/resources/app_icon.ico b/flutter.参考/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/flutter.参考/windows/runner/resources/app_icon.ico differ diff --git a/flutter.参考/windows/runner/runner.exe.manifest b/flutter.参考/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/flutter.参考/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/flutter.参考/windows/runner/utils.cpp b/flutter.参考/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/flutter.参考/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/flutter.参考/windows/runner/utils.h b/flutter.参考/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/flutter.参考/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/flutter.参考/windows/runner/win32_window.cpp b/flutter.参考/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/flutter.参考/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/flutter.参考/windows/runner/win32_window.h b/flutter.参考/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/flutter.参考/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/flutter.参考/データモデルのテンプレ.md b/flutter.参考/データモデルのテンプレ.md new file mode 100644 index 0000000..aa46134 --- /dev/null +++ b/flutter.参考/データモデルのテンプレ.md @@ -0,0 +1,8 @@ +// 将来、Odooの account.move と紐付けるための最小構成 +class InvoiceModel { + final String localUuid; // 端末で生成する唯一無二のID + final String? odooId; // Odooに書き込まれたら返ってくるID(最初はnull) + final DateTime createdAt; // 発行日 + final List items; // 明細 + // ... +} diff --git a/generated_pdfs/SELF001_20260220_210549.pdf b/generated_pdfs/SELF001_20260220_210549.pdf new file mode 100644 index 0000000..1ea0996 --- /dev/null +++ b/generated_pdfs/SELF001_20260220_210549.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (anonymous) /CreationDate (D:20260220210549+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260220210549+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 550 +>> +stream +Gatm94\s!M%,L^o)%-N+O2OV-7"P_"F_!jK5iVS=)fFm4RPpBlENN2?Z(IXQ?4q9l09u"\VfNB^]72"pm.tor*k^g2h.(q7f%I;Mei?#WY1"43-tqa:0%sY>BWds3Op.f=n+YG:s(K@:D^Hr,!DbG;@+5R/of=t4!d-:"/1eCZTp7p_6XWOnap9`@j-2%?0W4I]HW/TJ^0E5!WUPu_&0W.Z.a5ghj/X3acJD$+s&OLY"LLM)(!kX@B3'hNianhj&L=qaC6HWnG*-H%=_]C`s$]"P'M&dhj`aS/jfk=P9X]:EX,,Ru0N6#nZlEr_i'!g:W]YjK?V;[/<6B+Y1S'$`M!mArDngB\Dr!_cXsSp_OW0XU,NTpIj^qU$._A-)?Xfh=Nh@3u6=$<1`<[Z'"H8PUlIc5C`iQ8>)1dT#fXdacB3A_O]ZL0-a1LnDW@eMdLD]4C~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000000946 00000 n +0000001005 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1645 +%%EOF diff --git a/generated_pdfs/SELF001_20260220_210743.pdf b/generated_pdfs/SELF001_20260220_210743.pdf new file mode 100644 index 0000000..b8e54f6 --- /dev/null +++ b/generated_pdfs/SELF001_20260220_210743.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (anonymous) /CreationDate (D:20260220210743+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260220210743+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 550 +>> +stream +Gatm94\s!M%,L^o)%-N+O2OV-7"P_"F_!jK5iVS=)fFm4RPpBlENN2?Z(IXQ?4q9l09u"\VfNB^]72"pm.tor*k^g2h.(q7f%I;Mei?#WY1"43-tqa:0%sY>BWds3Op.f=n+YG:s(K@:D^Hr,!DbG;@+5R/of=t4!d-:"/1eCZTp7p_6XWOnap9`@j-2%?0W4I]HW/TJ^0E5!WUPu_&0W.Z.a5ghj/X3acJD$+s&OLY"LLM)(!kX@B3'hNianhj&L=qaC6HWnG*-H%=_]C`s$]"P'M&dhj`aS/jfk=P9X]:EX,,Ru0N6#nZlEr_i'!g:W]YjK?V;[/<6B+Y1S'$`M!mArDngB\Dr!_cXsSp_OW0XU,NTpIj^qU$._A-)?Xfh=Nh@3u6=$<1`<[Z'"H8PUlIc5C`iQ8>)1dT#fXdacB3A_O]ZL0-a1LnDW@eMdp<]4V~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000000946 00000 n +0000001005 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1645 +%%EOF diff --git a/generated_pdfs/SELF001_20260220_213646.pdf b/generated_pdfs/SELF001_20260220_213646.pdf new file mode 100644 index 0000000..4d2664e --- /dev/null +++ b/generated_pdfs/SELF001_20260220_213646.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (anonymous) /CreationDate (D:20260220213646+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260220213646+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 547 +>> +stream +Gatm94\rsL&Dd4649br/IFr2E,8*\+.3fcE"&d>BhmfIr)CJR!6k_#,I/!'ba0SJMmA9gZH#W2^F\f4%LgBo>aFe^(RsSjnrAm%6h)ZoSQZX35"9m&?a[Q)"!R::F+IReLUHNh(rN69>mmf"R!W*j]NQEpi_0BR\6CO%5%!OXoid-=jP4Kjg;=Wp7l[tuG"OQ]058KKeE9qqGoWMQ-a([Ys3+@-^-'i!3NYT-J5[X1i/ONgHRA':s^*,Y]kc6)BT#CJeT3mK&HWCS[N*A3i75>$X&ZrXQ5_<2M0K5^c+Z"*^i0P=qHf#$nk&9IHhsei\Z1FB+_%DS.,.O=K7SR+qQ@#!ffP~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000000946 00000 n +0000001005 00000 n +trailer +<< +/ID +[<240cd65002515f0922d2ce2f96f3ac0a><240cd65002515f0922d2ce2f96f3ac0a>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1642 +%%EOF diff --git a/generated_pdfs/SELF001_20260220_213944.pdf b/generated_pdfs/SELF001_20260220_213944.pdf new file mode 100644 index 0000000..8da8fb4 --- /dev/null +++ b/generated_pdfs/SELF001_20260220_213944.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (anonymous) /CreationDate (D:20260220213944+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260220213944+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 550 +>> +stream +Gatm94\rsL&Dd4649br/IFr2E,8*\+.3fcE!ts_qDd;'SXV2@Od6rj9hn=C2nSFCVH`;DM^RTZ;h8;$/@b6)ho&*gRih$!@=_Gd(=Q]hO\5$OiTV.]/\S-IATXfLh87nN4\(f`Krj*3?^R_;S"Y;+l)Nc>_8NRN17sl$q@p\=0GQO.St=5J=0t1-aYY-GR1B"5oCiCGaqY8)Wk"j@rEiD`KuJdKjH=hjJ.:+Bj&4*$@V'^KCCfcJUPGORcqHpV921Ym-=FSji>7SjP`+5>B[>NV]">b2E9$>VjDrpo:5&e;!6u9b'.3OJ;$!TVE4_=+rI24!Q+Q8LTfT+*1ZAfu8+M_Oendstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000000946 00000 n +0000001005 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1645 +%%EOF diff --git a/generated_pdfs/SELF001_20260220_214109.pdf b/generated_pdfs/SELF001_20260220_214109.pdf new file mode 100644 index 0000000..b669680 --- /dev/null +++ b/generated_pdfs/SELF001_20260220_214109.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (anonymous) /CreationDate (D:20260220214109+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260220214109+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (unspecified) /Title (untitled) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 550 +>> +stream +Gatm94\rsL&Dd4649br/IFr2E,8*\+.3fcE!ts_qDd;'SXV2@Od6rj9hn=C2nSFCVH`;DM^RTZ;h8;$/@b6)ho&*gRih$!@=_Gd(=Q]hO\5$OiTV.]/\S-IATXfLh87nN4\(f`Krj*3?^R_;S"Y;+l)NcKjH=hjJ.:+Bj&4*$@V'^KCCfcJUPGORcqHpV921Ym-=FSji>7SjP`+5>B[>NV]">b2E9$>VjDrpo:5&e;!6u9b'.3OJ;$!TVE4_=+rI24!Q+Q8LTfT+*1ZAfu8+M_Oendstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000000946 00000 n +0000001005 00000 n +trailer +<< +/ID +[<180ebe47f30d5a8825295f7ffa9e03ab><180ebe47f30d5a8825295f7ffa9e03ab>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1645 +%%EOF diff --git a/generated_pdfs/SELF001_20260220_221338.pdf b/generated_pdfs/SELF001_20260220_221338.pdf new file mode 100644 index 0000000..28300cf --- /dev/null +++ b/generated_pdfs/SELF001_20260220_221338.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260220221338+09'00') /Creator (anonymous) /Keywords (payload_hash=4cedce41aba36c7f3273e39954dbc1d7ce51d9d9ed3a929496933846d897bffc,chain_hash=9262891799ab99d6fedbdd9a4a410176e204d6f42901f4d48263b875e9874a9e,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260220221338+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (51b9ef4e-377c-4515-a263-1855cb2cb423) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0000\000-\0002\0002\0001\0003) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1101 +>> +stream +Gatm9D/\/e&H88.60OM:=<)U^84qdnXTLTu8V\/eMId?4M4QS,P,jqB6';3(PNp5TXZ4E@$a;&94nm_NO.7nOGlODsKY1TP?[h0k"'M]2QW0-e#P)bPdI./J]HW:6-L<Cf[LQpKJGAl,!61oLgula((jrIG-YW)1k`]Oe:BbQI>I_V51hG@^=d'hHcI!(o)ZAbN?+f'-9L7`0M2[%PuL-!7\JF#!usn,@mUTVpDSXTlN@BMC8:N')M,I\otPom7e.-ufLslM%M(ln3G3;,2o5u>Hb-JPqtNiqp@3kJ"JZCc4Q#XI^@VD_Cr/2M3GVFY$-&q[+eTqbro:2_bNPF^PkilG[rGHMb4rtVF]K8ajP=cnmNl`+Pe!jSgsuqKZIFfN_00,:D7qP@&+/"+X^#-VFFX$*GW7F&8)f!c,3>i+Ek@/9S%D0b9j9q/O.m^aDV&II6(.5Il'8GHaLo-qQeJ=kmd=GSJIcPt&9/9gK:oSFg>!E;gtZennifU6'fT]i\@?5DV5nc-_aWH31bQi(rr()7?(FsJ[eWdtrt/^@a(\R%AQrQu@^I@XjA0f`1n4*rIZON;qKXY;[U$c2";"WktC!*^KfL.f1b58d*"+F\ZQ-Jj19,1gNlS5flYnA="sobh-`.%H`^&o0":UuT8Thbh39gWHYI!)`eiLsERes<'X'E+=^BA=G1i+>h2p6ae#:X]orFX0eX)6B/u#fK4Q+E0XebQBeqoHc5O^6QiH#Y8aV'c5"QA/W9S5P_aMi@7BXC\~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<15dd0c4a48acbbc521db761dc01dde0b><15dd0c4a48acbbc521db761dc01dde0b>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2503 +%%EOF diff --git a/generated_pdfs/SELF001_20260220_223601.pdf b/generated_pdfs/SELF001_20260220_223601.pdf new file mode 100644 index 0000000..71a653b --- /dev/null +++ b/generated_pdfs/SELF001_20260220_223601.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260220223601+09'00') /Creator (anonymous) /Keywords (payload_hash=b6c6fa69eca77964dd3957484f88382fc21c57d971a2bf9d7f470493506fb399,chain_hash=be7d57eeca53eae26e234536678822e1b6e09396aa8b1f41a3aee8d0d1b0b291,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260220223601+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (61f634cf-cf01-445e-8388-20b5a7fedcd9) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0000\000-\0002\0002\0003\0006) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1189 +>> +stream +Gatm:?&tI7'RcT\JTtY(_6pJ+D0YC7G,;@AR9a[:d]-4h!iEjb5g%-GUMBf*?:[C*@F@Q7+9dAc\`\D]?\_bu0@kWn\Rk`FYhoB)/-aZL[?r)R>(!^60JS"-39F`[4nL4-mOu_Hf-ZK1q.*1f0+_Vo`i?E?9Q^A:d+6Nm`#L(N)/P.$D"e[IJhdeDj)9aSjW%h1h,,C;fd3]uK,_1N4i/eN!@(c^kVj&cO1nN,h+HR!i7m@LKf@heb@;XSSa8QTVucID:&P4gj]'Jha$BfOpR!(iDal?0aZ`e\61'H6_TgZ__N$&tWm?iQY*ZTCl!;&o@kA:b]FS*@f'1[NV4&_GU1OC5h?Vn*g(TepXY.OS@YCu-"a.e+5`Ko,=_#S0%D6ReR]$/A=@8U_(hB5sh>Y[A)jMTt7Q&a=<(Oti"2pgr;FK-1jh#-'CbH1@!R;Tr^,>3jKm9O^Ii>U,s91q#ga^MD#QGljF*GIk()=0q8;d[ddAjJh/Q`?QZJZ5uB'l+!67"abA56"U^u^qO,`rU?i$%))*cRN(TY2"(s:iLs2uLYI=H55b7@r:AaZ+7J(b(B3juh/2Z;Nluej'i/7=Uh0=7W]DX<40TfpI:L<7h3>IL_E=h;]O_DPlqbo(!7(*P0jYmtWf(Or5Eq.ejAD(>VQS>4GkRnT>#opU$B$;k5GW[XZTj`%eSFq^5El=4_>Sm+6D&2_,\fBDh;E6$\3Li1!LEiSb`af"F&Oo(o<^^'rF31?ij$^:',GH!1t@n*pg_\he7"cB[D=UV!fc)-UVER.RE`pJ'FULAKb?o2Vd@%0QIXH(K4(fL1`7R`Lu:NjNkm'b!(E*Zm,G4g@aN(kl;Z]V#I^bd=AX!nIOlZADW5)lpendstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<82fdff5d1007e609ea339247e34328f3><82fdff5d1007e609ea339247e34328f3>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2591 +%%EOF diff --git a/models/invoice_models.py b/models/invoice_models.py new file mode 100644 index 0000000..36f425a --- /dev/null +++ b/models/invoice_models.py @@ -0,0 +1,267 @@ +""" +伝票データモデル +Flutter参考プロジェクトの構造をFletに適用 +""" + +from enum import Enum +from datetime import datetime +from typing import List, Optional, Dict, Any +import json + +class DocumentType(Enum): + """帳票の種類を定義""" + ESTIMATE = "見積書" + DELIVERY = "納品書" + INVOICE = "請求書" + RECEIPT = "領収書" + SALES = "売上伝票" + +class InvoiceItem: + """伝票の各明細行を表すモデル""" + + def __init__(self, description: str, quantity: int, unit_price: int, is_discount: bool = False): + self.description = description + self.quantity = quantity + self.unit_price = unit_price + self.is_discount = is_discount # 値引き項目かどうかを示すフラグ + + @property + def subtotal(self) -> int: + """小計 (数量 * 単価)""" + return self.quantity * self.unit_price * (-1 if self.is_discount else 1) + + def copy_with(self, **kwargs) -> 'InvoiceItem': + """編集用のコピーメソッド""" + return InvoiceItem( + description=kwargs.get('description', self.description), + quantity=kwargs.get('quantity', self.quantity), + unit_price=kwargs.get('unit_price', self.unit_price), + is_discount=kwargs.get('is_discount', self.is_discount) + ) + + def to_dict(self) -> Dict[str, Any]: + """JSON変換""" + return { + 'description': self.description, + 'quantity': self.quantity, + 'unit_price': self.unit_price, + 'is_discount': self.is_discount + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'InvoiceItem': + """JSONから復元""" + return cls( + description=data['description'], + quantity=data['quantity'], + unit_price=data['unit_price'], + is_discount=data.get('is_discount', False) + ) + +class Customer: + """顧客情報モデル""" + + def __init__(self, id: int, name: str, formal_name: str, address: str = "", phone: str = ""): + self.id = id + self.name = name + self.formal_name = formal_name + self.address = address + self.phone = phone + + def to_dict(self) -> Dict[str, Any]: + """JSON変換""" + return { + 'id': self.id, + 'name': self.name, + 'formal_name': self.formal_name, + 'address': self.address, + 'phone': self.phone + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Customer': + """JSONから復元""" + return cls( + id=data['id'], + name=data['name'], + formal_name=data['formal_name'], + address=data.get('address', ''), + phone=data.get('phone', '') + ) + +class Invoice: + """帳票全体を管理するモデル (見積・納品・請求・領収に対応)""" + + def __init__(self, + customer: Customer, + date: datetime, + items: List[InvoiceItem], + invoice_number: Optional[str] = None, + notes: Optional[str] = None, + is_shared: bool = False, + document_type: DocumentType = DocumentType.INVOICE, + file_path: Optional[str] = None, + uuid: Optional[str] = None): + import uuid as uuid_module + self.uuid = uuid or str(uuid_module.uuid4()) + self.customer = customer + self.date = date + self.items = items + self.invoice_number = invoice_number or self._generate_invoice_number() + self.notes = notes + self.is_shared = is_shared + self.document_type = document_type + self.file_path = file_path + + def _generate_invoice_number(self) -> str: + """請求書番号を生成""" + return self.date.strftime('%Y%m%d-%H%M') + + @property + def client_name(self) -> str: + """互換性のためのゲッター""" + return self.customer.formal_name + + @property + def subtotal(self) -> int: + """税抜合計金額""" + return sum(item.subtotal for item in self.items) + + @property + def tax(self) -> int: + """消費税 (10%固定として計算、端数切り捨て)""" + return int(self.subtotal * 0.1) + + @property + def total_amount(self) -> int: + """税込合計金額""" + return self.subtotal + self.tax + + def copy_with(self, **kwargs) -> 'Invoice': + """状態更新のためのコピーメソッド""" + return Invoice( + customer=kwargs.get('customer', self.customer), + date=kwargs.get('date', self.date), + items=kwargs.get('items', self.items), + invoice_number=kwargs.get('invoice_number', self.invoice_number), + notes=kwargs.get('notes', self.notes), + is_shared=kwargs.get('is_shared', self.is_shared), + document_type=kwargs.get('document_type', self.document_type), + file_path=kwargs.get('file_path', self.file_path) + ) + + def to_csv(self) -> str: + """CSV形式への変換""" + lines = [ + f"Type,{self.document_type.value}", + f"Customer,{self.customer.formal_name}", + f"Number,{self.invoice_number}", + f"Date,{self.date.strftime('%Y/%m/%d')}", + f"Shared,{'Yes' if self.is_shared else 'No'}", + "", + "Description,Quantity,UnitPrice,Subtotal,IsDiscount" + ] + + for item in self.items: + lines.append(f"{item.description},{item.quantity},{item.unit_price},{item.subtotal},{'Yes' if item.is_discount else 'No'}") + + return '\n'.join(lines) + + def to_dict(self) -> Dict[str, Any]: + """JSON変換 (データベース保存用)""" + return { + 'uuid': self.uuid, + 'customer': self.customer.to_dict(), + 'date': self.date.isoformat(), + 'items': [item.to_dict() for item in self.items], + 'file_path': self.file_path, + 'invoice_number': self.invoice_number, + 'notes': self.notes, + 'is_shared': self.is_shared, + 'document_type': self.document_type.value + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Invoice': + """JSONから復元 (データベース読み込み用)""" + customer = Customer.from_dict(data['customer']) + date = datetime.fromisoformat(data['date']) + items = [InvoiceItem.from_dict(item_data) for item_data in data['items']] + + # DocumentTypeの文字列からEnumに変換 + doc_type_str = data.get('document_type', '請求書') + doc_type = next((dt for dt in DocumentType if dt.value == doc_type_str), DocumentType.INVOICE) + + return cls( + customer=customer, + date=date, + items=items, + invoice_number=data['invoice_number'], + notes=data.get('notes'), + is_shared=data.get('is_shared', False), + document_type=doc_type, + file_path=data.get('file_path'), + uuid=data.get('uuid') + ) + +# サンプルデータ生成関数 +def create_sample_invoices() -> List[Invoice]: + """サンプル伝票データを生成""" + customers = [ + Customer(1, "田中商事", "田中商事株式会社", "東京都千代田区丸の内1-1-1", "03-1234-5678"), + Customer(2, "鈴木商店", "鈴木商店", "東京都港区芝1-1-1", "03-2345-6789"), + Customer(3, "佐藤工業", "佐藤工業株式会社", "東京都品川区東品川1-1-1", "03-3456-7890"), + Customer(4, "高橋建設", "高橋建設株式会社", "東京都新宿区西新宿1-1-1", "03-4567-8901"), + Customer(5, "伊藤電機", "伊藤電機株式会社", "東京都渋谷区渋谷1-1-1", "03-5678-9012") + ] + + sample_invoices = [ + Invoice( + customer=customers[0], + date=datetime(2024, 1, 15), + items=[ + InvoiceItem("A商品セット", 1, 150000), + InvoiceItem("設置費用", 1, 25000) + ], + document_type=DocumentType.SALES, + notes="A商品セット販売" + ), + Invoice( + customer=customers[1], + date=datetime(2024, 1, 14), + items=[ + InvoiceItem("B部品", 10, 8500) + ], + document_type=DocumentType.ESTIMATE, + notes="B部品見積" + ), + Invoice( + customer=customers[2], + date=datetime(2024, 1, 13), + items=[ + InvoiceItem("C機器", 1, 120000) + ], + document_type=DocumentType.DELIVERY, + notes="C機器納品" + ), + Invoice( + customer=customers[3], + date=datetime(2024, 1, 12), + items=[ + InvoiceItem("D工事費用", 1, 200000) + ], + document_type=DocumentType.INVOICE, + notes="D工事請求" + ), + Invoice( + customer=customers[4], + date=datetime(2024, 1, 11), + items=[ + InvoiceItem("E製品", 1, 75000) + ], + document_type=DocumentType.RECEIPT, + notes="E製品領収" + ) + ] + + return sample_invoices diff --git a/sales_assist.db b/sales_assist.db new file mode 100644 index 0000000..13eebd3 Binary files /dev/null and b/sales_assist.db differ diff --git a/services/app_service.py b/services/app_service.py new file mode 100644 index 0000000..eb041f5 --- /dev/null +++ b/services/app_service.py @@ -0,0 +1,353 @@ +""" +アプリケーションサービス層 +UIとビジネスロジックの橋渡し +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from typing import Optional, List, Dict, Any +from datetime import datetime +from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType +from services.repositories import InvoiceRepository, CustomerRepository +from services.pdf_generator import PdfGenerator +import logging +import json +import hashlib +import os + +class InvoiceService: + """伝票ビジネスロジック""" + + def __init__(self): + self.invoice_repo = InvoiceRepository() + self.customer_repo = CustomerRepository() + self.pdf_generator = PdfGenerator() + self.company_info = self._load_company_info() + + def _load_company_info(self) -> Dict[str, str]: + """自社情報を読み込み""" + # TODO: 設定ファイルから読み込み + return { + 'id': 'SELF001', + 'name': '自社', + 'address': '', + 'phone': '', + 'registration_number': '' + } + + def _apply_audit_fields(self, invoice: Invoice, customer: Customer): + node_id = self.invoice_repo.get_node_id() + prev_chain_hash = self.invoice_repo.get_last_chain_hash(node_id) + + payload_obj = { + "schema": "invoice_payload_v1", + "node_id": node_id, + "uuid": invoice.uuid, + "document_type": invoice.document_type.value, + "invoice_number": invoice.invoice_number, + "date": invoice.date.replace(microsecond=0).isoformat(), + "tax_rate": 0.1, + "tax_calc_rule": "floor(subtotal * tax_rate)", + "customer_master_id": getattr(customer, "id", None), + "customer_snapshot": { + "name": customer.name, + "formal_name": customer.formal_name, + "address": customer.address, + "phone": customer.phone, + }, + "items": [ + { + "description": it.description, + "quantity": int(it.quantity), + "unit_price": int(it.unit_price), + "is_discount": bool(it.is_discount), + } + for it in invoice.items + ], + "notes": invoice.notes or "", + "pdf_template_version": "v1", + "company_info_version": "v1", + "is_offset": bool(getattr(invoice, "is_offset", False)), + "offset_target_uuid": getattr(invoice, "offset_target_uuid", None), + } + + payload_json = json.dumps(payload_obj, ensure_ascii=False, separators=(",", ":"), sort_keys=True) + payload_hash = hashlib.sha256(payload_json.encode("utf-8")).hexdigest() + chain_hash = hashlib.sha256(f"{prev_chain_hash}:{payload_hash}".encode("utf-8")).hexdigest() + + invoice.node_id = node_id + invoice.payload_json = payload_json + invoice.payload_hash = payload_hash + invoice.prev_chain_hash = prev_chain_hash + invoice.chain_hash = chain_hash + invoice.pdf_template_version = "v1" + invoice.company_info_version = "v1" + + def create_invoice(self, + customer: Customer, + document_type: DocumentType, + amount: int, + notes: str = "") -> Optional[Invoice]: + """新規伝票作成 + + Args: + customer: 顧客情報 + document_type: 帳票種類 + amount: 金額(税抜) + notes: 備考 + + Returns: + 作成されたInvoice、失敗時はNone + """ + try: + # 初期明細作成 + items = [InvoiceItem( + description=f"{document_type.value}分", + quantity=1, + unit_price=amount + )] + + # 伝票作成 + invoice = Invoice( + customer=customer, + date=datetime.now(), + items=items, + document_type=document_type, + notes=notes + ) + + # --- 長期保管向け: canonical payload + hash chain --- + self._apply_audit_fields(invoice, customer) + + # DB保存(PDFは仮生成物なので保存の成否と切り離す) + invoice.file_path = None + if self.invoice_repo.save_invoice(invoice): + logging.info(f"伝票作成成功: {invoice.invoice_number}") + else: + logging.error("伝票DB保存失敗") + return None + + # PDF生成(任意・仮生成物) + try: + pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info) + if pdf_path: + invoice.file_path = pdf_path + invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() + else: + logging.warning("PDF生成失敗(DB保存は完了)") + except Exception as e: + logging.warning(f"PDF生成例外(DB保存は完了): {e}") + + return invoice + + except Exception as e: + logging.error(f"伝票作成エラー: {e}") + return None + + def create_offset_invoice(self, target_uuid: str, notes: str = "") -> Optional[Invoice]: + """本伝を相殺する赤伝(マイナス伝票)を作成""" + try: + target = self.invoice_repo.get_invoice_by_uuid(target_uuid) + if not target: + logging.error(f"赤伝作成失敗: 対象uuidが見つかりません: {target_uuid}") + return None + + # 1行で相殺(値引き行としてマイナス) + items = [ + InvoiceItem( + description=f"相殺(赤伝) 対象:{target.invoice_number}", + quantity=1, + unit_price=int(target.subtotal), + is_discount=True, + ) + ] + + invoice = Invoice( + customer=target.customer, + date=datetime.now(), + items=items, + document_type=target.document_type, + notes=notes, + ) + invoice.is_offset = True + invoice.offset_target_uuid = target.uuid + + self._apply_audit_fields(invoice, invoice.customer) + + # DB保存(PDFは仮生成物) + invoice.file_path = None + if not self.invoice_repo.save_invoice(invoice): + logging.error("赤伝DB保存失敗") + return None + + # PDF生成(任意) + try: + pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info) + if pdf_path: + invoice.file_path = pdf_path + invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() + except Exception as e: + logging.warning(f"赤伝PDF生成例外(DB保存は完了): {e}") + + logging.info(f"赤伝作成成功: {invoice.invoice_number} (target={target.invoice_number})") + return invoice + + except Exception as e: + logging.error(f"赤伝作成エラー: {e}") + return None + + def get_recent_invoices(self, limit: int = 50) -> List[Invoice]: + """最近の伝票を取得""" + return self.invoice_repo.get_all_invoices(limit) + + def regenerate_pdf(self, invoice_uuid: str) -> Optional[str]: + """DBを正としてPDFを再生成(生成物は仮)""" + invoice = self.invoice_repo.get_invoice_by_uuid(invoice_uuid) + if not invoice: + logging.error(f"PDF再生成失敗: uuidが見つかりません: {invoice_uuid}") + return None + + # 会社情報は現状v1固定。将来はcompany_info_versionで分岐。 + pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info) + if not pdf_path: + return None + + return pdf_path + + def delete_pdf_file(self, pdf_path: str) -> bool: + """仮生成PDFを削除""" + try: + if not pdf_path: + return True + if os.path.exists(pdf_path): + os.remove(pdf_path) + return True + except Exception as e: + logging.warning(f"PDF削除失敗: {e}") + return False + + def delete_invoice(self, invoice_id: int) -> bool: + """伝票を削除""" + return self.invoice_repo.delete_invoice(invoice_id) + + def get_statistics(self) -> Dict[str, Any]: + """統計情報取得""" + return self.invoice_repo.get_statistics() + + def update_company_info(self, info: Dict[str, str]) -> bool: + """自社情報を更新""" + try: + self.company_info.update(info) + # TODO: 設定ファイルに保存 + return True + except Exception as e: + logging.error(f"自社情報更新エラー: {e}") + return False + + +class CustomerService: + """顧客ビジネスロジック""" + + def __init__(self): + self.customer_repo = CustomerRepository() + self._customer_cache: List[Customer] = [] + self._load_customers() + + def _load_customers(self): + """顧客データを読み込み""" + self._customer_cache = self.customer_repo.get_all_customers() + + # 初期データがない場合はサンプル作成 + if not self._customer_cache: + sample_customers = [ + Customer(1, "田中商事", "田中商事株式会社", "東京都千代田区丸の内1-1-1", "03-1234-5678"), + Customer(2, "鈴木商店", "鈴木商店", "東京都港区芝1-1-1", "03-2345-6789"), + Customer(3, "佐藤工業", "佐藤工業株式会社", "東京都品川区東品川1-1-1", "03-3456-7890"), + ] + for customer in sample_customers: + self.add_customer(customer) + + self._customer_cache = sample_customers + + def get_all_customers(self) -> List[Customer]: + """全顧客を取得""" + return self._customer_cache.copy() + + def search_customers(self, query: str) -> List[Customer]: + """顧客を検索""" + query = query.lower() + return [ + c for c in self._customer_cache + if query in c.name.lower() or + query in c.formal_name.lower() or + query in c.address.lower() + ] + + def add_customer(self, customer: Customer) -> bool: + """顧客を追加""" + success = self.customer_repo.save_customer(customer) + if success: + self._customer_cache.append(customer) + return success + + def delete_customer(self, customer_id: int) -> bool: + """顧客を削除""" + # TODO: 使用されている顧客は削除不可 + self._customer_cache = [c for c in self._customer_cache if c.id != customer_id] + return True + + +# サービスファクトリ +class AppService: + """アプリケーションサービス統合""" + + def __init__(self): + self.invoice = InvoiceService() + self.customer = CustomerService() + + def get_dashboard_data(self) -> Dict[str, Any]: + """ダッシュボード表示用データ""" + stats = self.invoice.get_statistics() + recent_invoices = self.invoice.get_recent_invoices(5) + + return { + 'total_invoices': stats['total_count'], + 'total_amount': stats['total_amount'], + 'monthly_amount': stats['monthly_amount'], + 'recent_invoices': recent_invoices, + 'customer_count': len(self.customer.get_all_customers()) + } + + +# 使用例 +if __name__ == "__main__": + # サービス初期化 + app_service = AppService() + + # ダッシュボードデータ取得 + dashboard = app_service.get_dashboard_data() + print(f"ダッシュボード: {dashboard}") + + # 顧客取得 + customers = app_service.customer.get_all_customers() + print(f"\n顧客数: {len(customers)}") + + if customers: + # 伝票作成テスト + from models.invoice_models import DocumentType + + invoice = app_service.invoice.create_invoice( + customer=customers[0], + document_type=DocumentType.INVOICE, + amount=250000, + notes="テスト伝票" + ) + + if invoice: + print(f"\n伝票作成成功: {invoice.invoice_number}") + print(f"合計金額: ¥{invoice.total_amount:,}") + print(f"PDF: {invoice.file_path}") + else: + print("\n伝票作成失敗") diff --git a/services/pdf_generator.py b/services/pdf_generator.py new file mode 100644 index 0000000..b6f97e3 --- /dev/null +++ b/services/pdf_generator.py @@ -0,0 +1,214 @@ +""" +PDF生成サービス +Flutter参考プロジェクトの機能をPythonで実装 +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import mm +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from datetime import datetime +from typing import Optional +from models.invoice_models import Invoice, InvoiceItem +import os + +class PdfGenerator: + """PDF生成サービス""" + + def __init__(self): + self.output_dir = "generated_pdfs" + os.makedirs(self.output_dir, exist_ok=True) + + # 日本語フォント登録(システムフォントを使用) + try: + pdfmetrics.registerFont(TTFont('Japanese', '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')) + except: + # フォールバック:デフォルトフォント + pass + + def generate_invoice_pdf(self, invoice: Invoice, company_info: dict) -> Optional[str]: + """請求書PDFを生成 + + Args: + invoice: 伝票データモデル + company_info: 自社情報 dict{name, address, phone, registration_number} + + Returns: + 生成されたPDFファイルパス、失敗時はNone + """ + try: + # ファイル名生成: {会社ID}_{端末ID}_{連番}.pdf + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{company_info.get('id', 'COMP')}_{timestamp}.pdf" + filepath = os.path.join(self.output_dir, filename) + + # PDF生成 + c = canvas.Canvas(filepath, pagesize=A4) + width, height = A4 + + # 監査向けのメタ情報(バイナリ同一性は要求しない) + try: + c.setAuthor(company_info.get("id", "")) + c.setTitle(f"{invoice.document_type.value} {invoice.invoice_number}") + c.setSubject(getattr(invoice, "uuid", "")) + keywords = [] + for key in ["payload_hash", "chain_hash", "node_id"]: + v = getattr(invoice, key, None) + if v: + keywords.append(f"{key}={v}") + if keywords: + c.setKeywords(",".join(keywords)) + except Exception: + pass + + # ヘッダー: 会社ロゴ・名前 + self._draw_header(c, invoice, company_info, width, height) + + # 顧客情報 + self._draw_customer_info(c, invoice, width, height) + + # 明細テーブル + self._draw_items_table(c, invoice, width, height) + + # 合計金額 + self._draw_totals(c, invoice, width, height) + + # フッター: 登録番号など + self._draw_footer(c, company_info, width) + + # payload_json をPDFに埋め込む(不可視テキストとして格納) + try: + payload_json = getattr(invoice, "payload_json", None) + if payload_json: + c.saveState() + c.setFillColorRGB(1, 1, 1) + c.setFont("Helvetica", 1) + # PDFのどこかに確実に残す(見えない) + c.drawString(1 * mm, 1 * mm, f"INVOICE_PAYLOAD_JSON:{payload_json}") + c.restoreState() + except Exception: + pass + + c.save() + return filepath + + except Exception as e: + print(f"PDF生成エラー: {e}") + return None + + def _draw_header(self, c: canvas.Canvas, invoice: Invoice, company_info: dict, width: float, height: float): + """ヘッダー描画""" + # 帳票種類タイトル + c.setFont("Helvetica-Bold", 24) + title = invoice.document_type.value + c.drawString(20*mm, height - 30*mm, title) + + # 自社情報 + c.setFont("Helvetica", 10) + company_name = company_info.get('name', '自社名未設定') + c.drawString(20*mm, height - 45*mm, f"{company_name}") + + # 日付・番号 + c.setFont("Helvetica", 10) + date_str = invoice.date.strftime("%Y年%m月%d日") + c.drawString(width - 80*mm, height - 30*mm, f"発行日: {date_str}") + c.drawString(width - 80*mm, height - 40*mm, f"No. {invoice.invoice_number}") + + def _draw_customer_info(self, c: canvas.Canvas, invoice: Invoice, width: float, height: float): + """顧客情報描画""" + y_pos = height - 70*mm + + c.setFont("Helvetica-Bold", 12) + c.drawString(20*mm, y_pos, "御中:") + + c.setFont("Helvetica", 11) + customer_name = invoice.customer.formal_name + c.drawString(20*mm, y_pos - 8*mm, customer_name) + + if invoice.customer.address: + c.setFont("Helvetica", 9) + c.drawString(20*mm, y_pos - 16*mm, invoice.customer.address) + + def _draw_items_table(self, c: canvas.Canvas, invoice: Invoice, width: float, height: float): + """明細テーブル描画""" + # テーブルヘッダー + y_start = height - 110*mm + col_x = [20*mm, 80*mm, 110*mm, 140*mm, 170*mm] + + # ヘッダーライン + c.setFont("Helvetica-Bold", 10) + headers = ["品名", "数量", "単価", "金額", "摘要"] + for i, header in enumerate(headers): + c.drawString(col_x[i], y_start, header) + + # 明細行 + c.setFont("Helvetica", 9) + y_pos = y_start - 10*mm + + for item in invoice.items: + c.drawString(col_x[0], y_pos, item.description[:20]) + c.drawRightString(col_x[1] + 20*mm, y_pos, str(item.quantity)) + c.drawRightString(col_x[2] + 20*mm, y_pos, f"¥{item.unit_price:,}") + c.drawRightString(col_x[3] + 20*mm, y_pos, f"¥{item.subtotal:,}") + y_pos -= 8*mm + + def _draw_totals(self, c: canvas.Canvas, invoice: Invoice, width: float, height: float): + """合計金額描画""" + y_pos = height - 180*mm + x_right = width - 40*mm + + c.setFont("Helvetica", 10) + c.drawRightString(x_right, y_pos, f"小計: ¥{invoice.subtotal:,}") + c.drawRightString(x_right, y_pos - 8*mm, f"消費税: ¥{invoice.tax:,}") + + c.setFont("Helvetica-Bold", 14) + c.drawRightString(x_right, y_pos - 20*mm, f"合計: ¥{invoice.total_amount:,}") + + def _draw_footer(self, c: canvas.Canvas, company_info: dict, width: float): + """フッター描画""" + y_pos = 30*mm + + c.setFont("Helvetica", 8) + reg_num = company_info.get('registration_number', '') + if reg_num: + c.drawString(20*mm, y_pos, f"登録番号: {reg_num}") + + # 支払期限・備考 + c.drawString(20*mm, y_pos - 5*mm, "お支払期限: 月末まで") + +# 使用例 +if __name__ == "__main__": + from models.invoice_models import Customer, InvoiceItem, DocumentType + from datetime import datetime + + # サンプルデータ + customer = Customer(1, "田中商事", "田中商事株式会社", "東京都千代田区", "03-1234-5678") + + invoice = Invoice( + customer=customer, + date=datetime.now(), + items=[ + InvoiceItem("商品A", 2, 15000), + InvoiceItem("商品B", 1, 30000), + ], + document_type=DocumentType.INVOICE, + ) + + company = { + 'id': 'SAMPLE001', + 'name': 'サンプル株式会社', + 'registration_number': 'T1234567890123' + } + + generator = PdfGenerator() + pdf_path = generator.generate_invoice_pdf(invoice, company) + + if pdf_path: + print(f"PDF生成成功: {pdf_path}") + else: + print("PDF生成失敗") diff --git a/services/repositories.py b/services/repositories.py new file mode 100644 index 0000000..f17760f --- /dev/null +++ b/services/repositories.py @@ -0,0 +1,586 @@ +""" +データリポジトリ層 +SQLiteデータベース操作を抽象化 +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import sqlite3 +import json +from datetime import datetime +from typing import List, Optional, Dict, Any +from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType +import logging +import uuid as uuid_module +import hashlib + +logging.basicConfig(level=logging.INFO) + +class InvoiceRepository: + """伝票データリポジトリ""" + + def __init__(self, db_path: str = "sales.db"): + self.db_path = db_path + self._init_tables() + + def _init_tables(self): + """テーブル初期化""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # メタ情報(node_id等) + cursor.execute( + ''' + CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT + ) + ''' + ) + + # 伝票テーブル + cursor.execute(''' + CREATE TABLE IF NOT EXISTS invoices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT UNIQUE, + document_type TEXT, + customer_id INTEGER, + customer_name TEXT, + customer_address TEXT, + customer_phone TEXT, + amount REAL, + tax REAL, + total_amount REAL, + date TEXT, + invoice_number TEXT, + notes TEXT, + file_path TEXT, + node_id TEXT, + payload_json TEXT, + payload_hash TEXT, + prev_chain_hash TEXT, + chain_hash TEXT, + pdf_template_version TEXT, + company_info_version TEXT, + is_offset INTEGER DEFAULT 0, + offset_target_uuid TEXT, + pdf_generated_at TEXT, + pdf_sha256 TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 明細テーブル + cursor.execute(''' + CREATE TABLE IF NOT EXISTS invoice_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + invoice_id INTEGER, + description TEXT, + quantity INTEGER, + unit_price INTEGER, + is_discount BOOLEAN, + FOREIGN KEY (invoice_id) REFERENCES invoices(id) + ) + ''') + + conn.commit() + + # 既存DB向けの軽量マイグレーション(列追加) + self._migrate_schema() + + def _migrate_schema(self): + """既存DBのスキーマを新仕様に追従(ALTER TABLEで列追加)""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # meta は _init_tables で作成済みだが念のため + cursor.execute( + ''' + CREATE TABLE IF NOT EXISTS meta ( + key TEXT PRIMARY KEY, + value TEXT + ) + ''' + ) + + def ensure_column(table: str, col: str, col_def: str): + cursor.execute(f"PRAGMA table_info({table})") + cols = {r[1] for r in cursor.fetchall()} + if col not in cols: + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {col_def}") + + ensure_column("invoices", "customer_id", "customer_id INTEGER") + ensure_column("invoices", "node_id", "node_id TEXT") + ensure_column("invoices", "payload_json", "payload_json TEXT") + ensure_column("invoices", "payload_hash", "payload_hash TEXT") + ensure_column("invoices", "prev_chain_hash", "prev_chain_hash TEXT") + ensure_column("invoices", "chain_hash", "chain_hash TEXT") + ensure_column("invoices", "pdf_template_version", "pdf_template_version TEXT") + ensure_column("invoices", "company_info_version", "company_info_version TEXT") + ensure_column("invoices", "is_offset", "is_offset INTEGER DEFAULT 0") + ensure_column("invoices", "offset_target_uuid", "offset_target_uuid TEXT") + ensure_column("invoices", "pdf_generated_at", "pdf_generated_at TEXT") + ensure_column("invoices", "pdf_sha256", "pdf_sha256 TEXT") + + conn.commit() + + def get_node_id(self) -> str: + """DB単位のnode_id(UUID)を取得(未設定なら生成して保存)""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM meta WHERE key = ?", ("node_id",)) + row = cursor.fetchone() + if row and row[0]: + return row[0] + + node_id = str(uuid_module.uuid4()) + cursor.execute( + "INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)", + ("node_id", node_id), + ) + conn.commit() + return node_id + + def get_last_chain_hash(self, node_id: str) -> str: + """直前のchain_hashを取得(無ければgenesis=all-zero)""" + genesis = "0" * 64 + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT chain_hash FROM invoices WHERE node_id = ? AND chain_hash IS NOT NULL ORDER BY id DESC LIMIT 1", + (node_id,), + ) + row = cursor.fetchone() + return row[0] if row and row[0] else genesis + except Exception: + return genesis + + def verify_chain(self, node_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]: + """DB内の連鎖ハッシュ整合性を検証(監査用) + + Returns: + { + 'node_id': str, + 'checked': int, + 'ok': bool, + 'errors': [ { ... } ] + } + """ + if not node_id: + node_id = self.get_node_id() + + genesis = "0" * 64 + errors: List[Dict[str, Any]] = [] + checked = 0 + + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + sql = "SELECT id, uuid, node_id, payload_json, payload_hash, prev_chain_hash, chain_hash FROM invoices WHERE node_id = ? ORDER BY id ASC" + params = [node_id] + if limit is not None: + sql += " LIMIT ?" + params.append(int(limit)) + + cursor.execute(sql, tuple(params)) + rows = cursor.fetchall() + + expected_prev = genesis + for (inv_id, inv_uuid, inv_node_id, payload_json, payload_hash, prev_chain_hash, chain_hash) in rows: + checked += 1 + + if not payload_json: + errors.append( + { + "invoice_id": inv_id, + "uuid": inv_uuid, + "type": "missing_payload_json", + } + ) + # 検証不能なのでチェーンはそこで停止扱い + break + + expected_payload_hash = hashlib.sha256(payload_json.encode("utf-8")).hexdigest() + if payload_hash != expected_payload_hash: + errors.append( + { + "invoice_id": inv_id, + "uuid": inv_uuid, + "type": "payload_hash_mismatch", + "db": payload_hash, + "expected": expected_payload_hash, + } + ) + break + + if (prev_chain_hash or genesis) != expected_prev: + errors.append( + { + "invoice_id": inv_id, + "uuid": inv_uuid, + "type": "prev_chain_hash_mismatch", + "db": prev_chain_hash, + "expected": expected_prev, + } + ) + break + + expected_chain_hash = hashlib.sha256(f"{expected_prev}:{expected_payload_hash}".encode("utf-8")).hexdigest() + if chain_hash != expected_chain_hash: + errors.append( + { + "invoice_id": inv_id, + "uuid": inv_uuid, + "type": "chain_hash_mismatch", + "db": chain_hash, + "expected": expected_chain_hash, + } + ) + break + + expected_prev = expected_chain_hash + + except Exception as e: + errors.append({"type": "exception", "message": str(e)}) + + return { + "node_id": node_id, + "checked": checked, + "ok": len(errors) == 0, + "errors": errors, + } + + def save_invoice(self, invoice: Invoice) -> bool: + """伝票を保存""" + try: + node_id = getattr(invoice, "node_id", None) or self.get_node_id() + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # メインデータ挿入 + cursor.execute(''' + INSERT INTO invoices + (uuid, document_type, customer_id, customer_name, customer_address, + customer_phone, amount, tax, total_amount, date, + invoice_number, notes, file_path, node_id, + payload_json, payload_hash, prev_chain_hash, chain_hash, + pdf_template_version, company_info_version, + is_offset, offset_target_uuid, + pdf_generated_at, pdf_sha256) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + invoice.uuid, + invoice.document_type.value, + getattr(invoice.customer, "id", None), + invoice.customer.formal_name, + invoice.customer.address, + invoice.customer.phone, + invoice.subtotal, + invoice.tax, + invoice.total_amount, + invoice.date.isoformat(), + invoice.invoice_number, + invoice.notes, + invoice.file_path, + node_id, + getattr(invoice, "payload_json", None), + getattr(invoice, "payload_hash", None), + getattr(invoice, "prev_chain_hash", None), + getattr(invoice, "chain_hash", None), + getattr(invoice, "pdf_template_version", None), + getattr(invoice, "company_info_version", None), + 1 if getattr(invoice, "is_offset", False) else 0, + getattr(invoice, "offset_target_uuid", None), + getattr(invoice, "pdf_generated_at", None), + getattr(invoice, "pdf_sha256", None), + )) + + invoice_id = cursor.lastrowid + + # 明細データ挿入 + for item in invoice.items: + cursor.execute(''' + INSERT INTO invoice_items + (invoice_id, description, quantity, unit_price, is_discount) + VALUES (?, ?, ?, ?, ?) + ''', ( + invoice_id, + item.description, + item.quantity, + item.unit_price, + item.is_discount + )) + + conn.commit() + return True + + except Exception as e: + logging.error(f"伝票保存エラー: {e}") + return False + + def get_all_invoices(self, limit: int = 100) -> List[Invoice]: + """全伝票を取得""" + invoices = [] + + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM invoices + ORDER BY date DESC + LIMIT ? + ''', (limit,)) + + rows = cursor.fetchall() + + for row in rows: + invoice = self._row_to_invoice(row, cursor) + if invoice: + invoices.append(invoice) + + except Exception as e: + logging.error(f"伝票取得エラー: {e}") + + return invoices + + def get_invoice_by_uuid(self, invoice_uuid: str) -> Optional[Invoice]: + """UUIDで伝票を取得(PDF再生成等で使用)""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + "SELECT * FROM invoices WHERE uuid = ? LIMIT 1", + (invoice_uuid,), + ) + row = cursor.fetchone() + if not row: + return None + return self._row_to_invoice(row, cursor) + except Exception as e: + logging.error(f"UUID取得エラー: {e}") + return None + + def _row_to_invoice(self, row: tuple, cursor: sqlite3.Cursor) -> Optional[Invoice]: + """DB行をInvoiceオブジェクトに変換""" + try: + invoice_id = row[0] + + # 明細取得 + cursor.execute(''' + SELECT description, quantity, unit_price, is_discount + FROM invoice_items + WHERE invoice_id = ? + ''', (invoice_id,)) + + item_rows = cursor.fetchall() + items = [ + InvoiceItem( + description=ir[0], + quantity=ir[1], + unit_price=ir[2], + is_discount=bool(ir[3]) + ) for ir in item_rows + ] + + # 顧客情報 + customer = Customer( + id=row[3] or 0, + name=row[4], + formal_name=row[4], + address=row[5] or "", + phone=row[6] or "" + ) + + # 伝票タイプ + doc_type = DocumentType.SALES + for dt in DocumentType: + if dt.value == row[2]: + doc_type = dt + break + + inv = Invoice( + customer=customer, + date=datetime.fromisoformat(row[10]), + items=items, + file_path=row[13], + invoice_number=row[11] or "", + notes=row[12], + document_type=doc_type, + uuid=row[1], + ) + + # 監査用フィールド(存在していれば付与) + try: + inv.node_id = row[14] + inv.payload_json = row[15] + inv.payload_hash = row[16] + inv.prev_chain_hash = row[17] + inv.chain_hash = row[18] + inv.pdf_template_version = row[19] + inv.company_info_version = row[20] + inv.is_offset = bool(row[21]) + inv.offset_target_uuid = row[22] + inv.pdf_generated_at = row[23] + inv.pdf_sha256 = row[24] + except Exception: + pass + + return inv + + except Exception as e: + logging.error(f"変換エラー: {e}") + return None + + def delete_invoice(self, invoice_id: int) -> bool: + """伝票を削除""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # 明細を先に削除 + cursor.execute('DELETE FROM invoice_items WHERE invoice_id = ?', (invoice_id,)) + + # 伝票を削除 + cursor.execute('DELETE FROM invoices WHERE id = ?', (invoice_id,)) + + conn.commit() + return True + + except Exception as e: + logging.error(f"削除エラー: {e}") + return False + + def get_statistics(self) -> Dict[str, Any]: + """統計情報取得""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # 総数 + cursor.execute('SELECT COUNT(*) FROM invoices') + total_count = cursor.fetchone()[0] + + # 総売上 + cursor.execute('SELECT SUM(total_amount) FROM invoices') + total_amount = cursor.fetchone()[0] or 0 + + # 今月の売上 + today = datetime.now() + first_day = today.replace(day=1).isoformat() + cursor.execute(''' + SELECT SUM(total_amount) FROM invoices + WHERE date >= ? + ''', (first_day,)) + monthly_amount = cursor.fetchone()[0] or 0 + + return { + 'total_count': total_count, + 'total_amount': total_amount, + 'monthly_amount': monthly_amount + } + + except Exception as e: + logging.error(f"統計エラー: {e}") + return {'total_count': 0, 'total_amount': 0, 'monthly_amount': 0} + + +class CustomerRepository: + """顧客リポジトリ""" + + def __init__(self, db_path: str = "sales.db"): + self.db_path = db_path + self._init_tables() + + def _init_tables(self): + """テーブル初期化""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + formal_name TEXT, + address TEXT, + phone TEXT, + email TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() + + def save_customer(self, customer: Customer) -> bool: + """顧客を保存""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + cursor.execute(''' + INSERT OR REPLACE INTO customers + (id, name, formal_name, address, phone) + VALUES (?, ?, ?, ?, ?) + ''', ( + customer.id, + customer.name, + customer.formal_name, + customer.address, + customer.phone + )) + + conn.commit() + return True + + except Exception as e: + logging.error(f"顧客保存エラー: {e}") + return False + + def get_all_customers(self) -> List[Customer]: + """全顧客を取得""" + customers = [] + + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM customers ORDER BY formal_name') + + rows = cursor.fetchall() + for row in rows: + customers.append(Customer( + id=row[0], + name=row[1], + formal_name=row[2], + address=row[3] or "", + phone=row[4] or "" + )) + + except Exception as e: + logging.error(f"顧客取得エラー: {e}") + + return customers + + +# 使用例 +if __name__ == "__main__": + from models.invoice_models import create_sample_invoices + + # リポジトリ初期化 + invoice_repo = InvoiceRepository() + + # サンプルデータ保存 + sample_invoices = create_sample_invoices() + for invoice in sample_invoices: + success = invoice_repo.save_invoice(invoice) + print(f"保存: {invoice.customer.formal_name} - {'成功' if success else '失敗'}") + + # 統計取得 + stats = invoice_repo.get_statistics() + print(f"\n統計: {stats}") + + # 全伝票取得 + all_invoices = invoice_repo.get_all_invoices() + print(f"\n全伝票数: {len(all_invoices)}") diff --git a/test_force_size.py b/test_force_size.py new file mode 100644 index 0000000..874bc91 --- /dev/null +++ b/test_force_size.py @@ -0,0 +1,44 @@ +""" +強制ウィンドウサイズテスト +""" + +import flet as ft +import time + +def main(page: ft.Page): + page.title = "強制サイズテスト" + + # 複数の方法で試す + page.window.width = 350 + page.window.height = 700 + page.window.resizable = False + page.window_center = True + + print(f"設定値: {page.window.width} x {page.window.height}") + + # 遅延してから再度設定 + def delayed_resize(): + time.sleep(0.5) + page.window.width = 350 + page.window.height = 700 + page.update() + print("遅延設定完了") + + page.add( + ft.Column([ + ft.Text("強制サイズテスト", size=18, weight=ft.FontWeight.BOLD), + ft.Text(f"設定: 350 x 700"), + ft.Text("表示は?"), + ft.Button("遅延リサイズ", on_click=lambda _: delayed_resize()), + ft.Container( + content=ft.Text("テストコンテンツ", color=ft.Colors.WHITE), + bgcolor=ft.Colors.BLUE, + padding=15, + width=300, + height=100 + ) + ], spacing=10) + ) + +if __name__ == "__main__": + ft.run(main) diff --git a/test_size_1.py b/test_size_1.py new file mode 100644 index 0000000..2a894fa --- /dev/null +++ b/test_size_1.py @@ -0,0 +1,39 @@ +""" +ウインドウサイズテスト - 手法1: page.windowプロパティ +""" + +import flet as ft + +def main(page: ft.Page): + # ウィンドウサイズ設定 - 手法1 + page.title = "サイズテスト1" + page.window_width = 300 + page.window_height = 500 + page.window_min_width = 250 + page.window_min_height = 400 + page.window_max_width = 400 + page.window_max_height = 600 + page.window_resizable = True + page.window_center = True + + # コンテンツ + page.add( + ft.Column([ + ft.Text("手法1: page.windowプロパティ", size=20, weight=ft.FontWeight.BOLD), + ft.Text(f"ウィンドウ幅: {page.window_width}"), + ft.Text(f"ウィンドウ高: {page.window_height}"), + ft.Text(f"最小幅: {page.window_min_width}"), + ft.Text(f"最小高: {page.window_min_height}"), + ft.ElevatedButton("テストボタン", on_click=lambda _: print("クリックされました")), + ft.Container( + content=ft.Text("コンテナテスト", color=ft.Colors.WHITE), + bgcolor=ft.Colors.BLUE, + padding=20, + width=200, + height=100 + ) + ], spacing=10) + ) + +if __name__ == "__main__": + ft.run(main) diff --git a/test_size_2.py b/test_size_2.py new file mode 100644 index 0000000..fc30ce6 --- /dev/null +++ b/test_size_2.py @@ -0,0 +1,40 @@ +""" +ウインドウサイズテスト - 手法2: window_setter +""" + +import flet as ft + +def main(page: ft.Page): + # ウィンドウサイズ設定 - 手法2 + page.title = "サイズテスト2" + + # window_setterを使用 + page.window_setter( + width=300, + height=500, + min_width=250, + min_height=400, + max_width=400, + max_height=600, + resizable=True, + center=True + ) + + # コンテンツ + page.add( + ft.Column([ + ft.Text("手法2: window_setter", size=20, weight=ft.FontWeight.BOLD), + ft.Text("window_setter()を使用"), + ft.ElevatedButton("テストボタン", on_click=lambda _: print("クリックされました")), + ft.Container( + content=ft.Text("コンテナテスト", color=ft.Colors.WHITE), + bgcolor=ft.Colors.GREEN, + padding=20, + width=200, + height=100 + ) + ], spacing=10) + ) + +if __name__ == "__main__": + ft.run(main) diff --git a/test_size_3.py b/test_size_3.py new file mode 100644 index 0000000..693cb8c --- /dev/null +++ b/test_size_3.py @@ -0,0 +1,39 @@ +""" +ウインドウサイズテスト - 手法3: page.windowプロパティ(修正版) +""" + +import flet as ft + +def main(page: ft.Page): + # ウィンドウサイズ設定 - 手法3(修正版) + page.title = "サイズテスト3" + + # page.windowプロパティを個別に設定 + page.window.width = 300 + page.window.height = 500 + page.window.min_width = 250 + page.window.min_height = 400 + page.window.max_width = 400 + page.window.max_height = 600 + page.window.resizable = True + # 中央配置はpage.window_centerを使用 + page.window_center = True + + # コンテンツ + page.add( + ft.Column([ + ft.Text("手法3: page.windowプロパティ(修正版)", size=20, weight=ft.FontWeight.BOLD), + ft.Text("page.window.width/heightを個別設定"), + ft.ElevatedButton("テストボタン", on_click=lambda _: print("クリックされました")), + ft.Container( + content=ft.Text("コンテナテスト", color=ft.Colors.WHITE), + bgcolor=ft.Colors.RED, + padding=20, + width=200, + height=100 + ) + ], spacing=10) + ) + +if __name__ == "__main__": + ft.run(main) diff --git a/test_size_4.py b/test_size_4.py new file mode 100644 index 0000000..95d8d89 --- /dev/null +++ b/test_size_4.py @@ -0,0 +1,36 @@ +""" +ウインドウサイズテスト - 手法4: ft.run()オプション +""" + +import flet as ft + +def main(page: ft.Page): + # ウィンドウサイズ設定 - 手法4 + page.title = "サイズテスト4" + + # 基本的なプロパティ設定 + page.window.width = 300 + page.window.height = 500 + + # コンテンツ + page.add( + ft.Column([ + ft.Text("手法4: ft.run()オプション", size=20, weight=ft.FontWeight.BOLD), + ft.Text("ft.run()のviewオプションを使用"), + ft.ElevatedButton("テストボタン", on_click=lambda _: print("クリックされました")), + ft.Container( + content=ft.Text("コンテナテスト", color=ft.Colors.WHITE), + bgcolor=ft.Colors.PURPLE, + padding=20, + width=200, + height=100 + ) + ], spacing=10) + ) + +if __name__ == "__main__": + # ft.run()のオプションでウィンドウサイズを指定 + ft.run( + main, + view=ft.AppView.WEB_BROWSER + ) diff --git a/test_size_5.py b/test_size_5.py new file mode 100644 index 0000000..9ec917f --- /dev/null +++ b/test_size_5.py @@ -0,0 +1,32 @@ +""" +ウインドウサイズテスト - 手法5: 最もシンプル +""" + +import flet as ft + +def main(page: ft.Page): + # ウィンドウサイズ設定 - 手法5(最もシンプル) + page.title = "サイズテスト5" + page.window.width = 300 + page.window.height = 500 + + # コンテンツ + page.add( + ft.Column([ + ft.Text("手法5: page.windowプロパティ(シンプル)", size=18, weight=ft.FontWeight.BOLD), + ft.Text("page.window.width = 300"), + ft.Text("page.window.height = 500"), + ft.Button("テストボタン", on_click=lambda _: print("クリックされました")), + ft.Container( + content=ft.Text("コンテナテスト", color=ft.Colors.WHITE), + bgcolor=ft.Colors.ORANGE, + padding=20, + width=200, + height=100 + ), + ft.Text("この手法が最も確実", size=14, color=ft.Colors.GREEN) + ], spacing=10) + ) + +if __name__ == "__main__": + ft.run(main)