diff --git a/README.md b/README.md index 4e2b5cc..04257e5 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,149 @@ # 販売アシスト1号 -Python + Fletで開発したAndroid対応販売管理アプリケーションです。 +Python + Flet で開発した、Android向けスタンドアロン販売管理アプリです。 -## 機能 +## 主な機能 -- **ダッシュボード**: 顧客数、商品数、売上件数、総売上を表示 -- **顧客管理**: 顧客情報の追加、編集、削除 -- **商品管理**: 商品情報の追加、編集、削除、在庫管理 -- **売上管理**: 売上データの記録と閲覧 -- **データ出力**: JSON/CSV形式でのデータエクスポート -- **電子帳簿保存法対応**: 10年間データ保持、監査証跡、整合性チェック - -## 電子帳簿保存法対応 - -- **10年間データ保持**: 法定期間のデータ保存に対応 -- **監査証跡**: 全データ操作のログ記録 -- **データ整合性**: チェックサムによる改ざん検知 -- **アーカイブ機能**: 7年以上前のデータを自動アーカイブ -- **コンプライアンスレポート**: 法令対応状況の定期報告 +- ダッシュボード(顧客数・商品数・売上件数・総売上) +- 顧客管理(追加・編集・削除) +- 商品管理(追加・編集・削除・在庫管理) +- 売上管理(記録・閲覧) +- データ出力(JSON/CSV) ## セットアップ -1. 依存関係をインストール: ```bash +python -m venv .venv +source .venv/bin/activate pip install -r requirements.txt ``` -2. アプリケーションを実行: +## 実行 + ```bash python main.py ``` +## 伝票エクスプローラー(一覧) + +伝票一覧画面で以下の操作ができます。 + +- 検索: 伝票番号 / 顧客名 / 種別 / 備考 +- 期間: 直近7日 / 30日 / 3ヶ月 / 1年 / 全期間 +- ソート: 日付 / 伝票番号 / 顧客名 / 種別 / 更新日時 +- 並び順切替: 昇順 / 降順 +- 赤伝の表示切替 +- ページ送り(前 / 次) +- 「マスタ編集」ボタンから顧客/商品マスタ編集へ遷移 + +大量データ(7年分想定)でも、SQLiteの条件検索とページングで段階的に表示します。 + +## 伝票エディタ(明細編集) + +- 明細は行追加 / 行削除で編集 +- 保存時に明細を正規化(空行除去、数量補正) +- 保存時バリデーション: + - 商品名必須 + - 数量は1以上 + - 単価は0以上 + +不正な行がある場合は保存せず、先頭エラーを画面通知します。 + +再利用用の最小フレーム: + +- `components/explorer_framework.py`(検索状態・期間・ソート) +- `components/editor_framework.py`(明細正規化・検証・表示/編集テーブル) +- `components/universal_master_editor.py`(顧客/商品マスタで検索・ソート・ページングを共通利用) + ## Androidビルド -Fletを使用してAndroidアプリをビルド: +### 直接コマンド ```bash -flet build apk . +flet build apk . --module-name main ``` -リリースAABを作る場合: +リリース用AAB: + ```bash -flet build aab . +flet build aab . --module-name main ``` +### build.py を使う場合 + +```bash +python build.py apk +python build.py aab +``` + +## APKインストール + +### Androidエミュレータ + +- 方法A: 生成された APK をエミュレータへドラッグ&ドロップ +- 方法B: + +```bash +adb install -r +``` + +## トラブルシュート + +### 1. 実機で起動時に落ちる + +```bash +adb logcat -c +adb logcat +``` + +必要箇所だけ抽出: + +```bash +adb logcat | grep -E "FATAL EXCEPTION|Traceback|Python|Chaquopy|sqlite|Permission denied|No such file" +``` + +### 2. Fletコマンドが見つからない + +```bash +source .venv/bin/activate +pip install -r requirements.txt +``` + +### 3. ビルドが失敗する + +- Python/Flet バージョンを確認 +- Android SDK / JDK の設定を確認 +- 失敗ログ全文を保存して原因行を確認 + +## データ保存 + +SQLite (`sales.db`) を使用します。主なテーブル: + +- `customers` +- `products` +- `sales` +- `audit_logs` +- `integrity_checks` +- `archive_sales` + +## 電子帳簿保存法対応(要点) + +- 取引データの長期保存(10年) +- 監査証跡の記録 +- 整合性チェック(改ざん検知) +- 検索・閲覧可能な形式での保管 + ## リポジトリ整理の自動化 -SWE実行で生成された試作ファイル/生成物を安全に整理するため、 -削除ではなく `trash/` へ隔離するスクリプトを用意しています。 +生成物や試作ファイルを削除せず `trash/` に隔離するスクリプト: ```bash bash scripts/auto_recover_and_build.sh /home/user/dev/h-1.flet.3 ``` -このスクリプトで実行される内容: +実行内容: -- プロジェクト全体のバックアップ作成 -- 生成物/試作ファイルの `trash//` への移動 -- `.gitignore` の整備 -- Gitベースラインコミット作成(必要時) - -注意: - -- 実行確認 (`python main.py`) と APK ビルド (`flet build apk`) は自動実行しません -- 必要に応じて最後に表示されるコマンドを手動実行してください - -## データベース - -アプリケーションはSQLiteデータベース(`sales.db`)を使用してデータを保存します。 - -- `customers`: 顧客情報 -- `products`: 商品情報 -- `sales`: 売上データ -- `audit_logs`: 監査ログ -- `integrity_checks`: 整合性チェック記録 -- `archive_sales`: アーカイブ済み売上データ - -## 使用方法 - -1. アプリを起動するとダッシュボードが表示されます -2. 左側のナビゲーションレールで各機能にアクセス -3. 各画面で「追加」ボタンから新しいデータを登録 -4. 編集・削除ボタンで既存データを管理 -5. 「データ出力」でバックアップ作成 -6. 「コンプライアンス」で法令対応管理 - -## 電子帳簿保存法要件 - -- **検索要件**: 任意の項目でデータ検索可能 -- **日付要件**: 取引日時の正確な記録 -- **金額要件**: 取引金額の正確な記録 -- **署名要件**: 電子署名(チェックサム)による改ざん防止 -- **保存期間**: 10年間のデータ保持 -- **可視性要件**: 随時閲覧可能な形式 - -## 技術仕様 - -- **フレームワーク**: Flet -- **言語**: Python 3.8+ -- **データベース**: SQLite -- **UI**: モダンなマテリアルデザイン -- **対応OS**: Android, iOS, Windows, macOS, Linux -- **オフライン動作**: 完全スタンドアローン - -## 法令対応 - -電子帳簿保存法のすべての要件を満たす設計: -- 完全な監査証跡の保持 -- データ改ざん防止機能 -- 10年間の長期保存 -- 検索・閲覧の容易性 -- 定期的な整合性検証 +- プロジェクトバックアップ作成 +- 生成物の `trash//` 移動 +- `.gitignore` 整備 +- Git ベースラインコミット作成(必要時) diff --git a/app_compiz_fixed.py b/app_compiz_fixed.py deleted file mode 100644 index 6eb5867..0000000 --- a/app_compiz_fixed.py +++ /dev/null @@ -1,807 +0,0 @@ -import flet as ft -import sqlite3 -import signal -import sys -import logging -import random -from datetime import datetime -from typing import List, Dict, Optional - -class ErrorHandler: - """グローバルエラーハンドラ""" - - @staticmethod - def handle_error(error: Exception, context: str = ""): - """エラーを一元処理""" - error_msg = f"{context}: {str(error)}" - logging.error(error_msg) - print(f"❌ {error_msg}") - - try: - if hasattr(ErrorHandler, 'current_page'): - ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) - except: - pass - - @staticmethod - def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): - """SnackBarを表示""" - try: - page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - page.snack_bar.open = True - page.update() - except: - pass - -class DummyDataGenerator: - """テスト用ダミーデータ生成""" - - @staticmethod - def generate_customers(count: int = 100) -> List[Dict]: - """ダミー顧客データ生成""" - first_names = ["田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本", "中村", "小林", "加藤"] - last_names = ["太郎", "次郎", "三郎", "花子", "美子", "健一", "恵子", "大輔", "由美", "翔太"] - - customers = [] - for i in range(count): - name = f"{random.choice(first_names)} {random.choice(last_names)}" - customers.append({ - 'id': i + 1, - 'name': name, - 'phone': f"090-{random.randint(1000, 9999)}-{random.randint(1000, 9999)}", - 'email': f"customer{i+1}@example.com", - 'address': f"東京都{random.choice(['渋谷区', '新宿区', '港区', '千代田区'])}{random.randint(1, 50)}-{random.randint(1, 10)}" - }) - return customers - - @staticmethod - def generate_products(count: int = 50) -> List[Dict]: - """ダミー商品データ生成""" - categories = ["電子機器", "衣料品", "食品", "書籍", "家具"] - products = [] - - for i in range(count): - category = random.choice(categories) - products.append({ - 'id': i + 1, - 'name': f"{category}{i+1}", - 'category': category, - 'price': random.randint(100, 50000), - 'stock': random.randint(0, 100) - }) - return products - - @staticmethod - def generate_sales(count: int = 200) -> List[Dict]: - """ダミー売上データ生成""" - customers = DummyDataGenerator.generate_customers(20) - products = DummyDataGenerator.generate_products(30) - - sales = [] - for i in range(count): - customer = random.choice(customers) - product = random.choice(products) - quantity = random.randint(1, 10) - - sales.append({ - 'id': i + 1, - 'customer_id': customer['id'], - 'customer_name': customer['name'], - 'product_id': product['id'], - 'product_name': product['name'], - 'quantity': quantity, - 'unit_price': product['price'], - 'total_price': quantity * product['price'], - 'date': datetime.now().strftime("%Y-%m-%d"), - 'created_at': datetime.now().isoformat() - }) - return sales - -class DashboardView(ft.Column): - """ダッシュボードビュー""" - - def __init__(self, page: ft.Page): - super().__init__(expand=True, spacing=15, visible=False) - # pageプロパティを設定 - object.__setattr__(self, 'page', page) - - # UI構築 - self.title = ft.Text("ダッシュボード", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # 統計カード - self.stats_row1 = ft.Row([], spacing=10) - self.stats_row2 = ft.Row([], spacing=10) - - # キーボードショートカット表示 - self.shortcuts = ft.Container( - content=ft.Column([ - ft.Text("キーボードショートカット", size=16, weight=ft.FontWeight.BOLD), - ft.Text("1: ダッシュボード", size=14), - ft.Text("2: 売上管理", size=14), - ft.Text("3: 顧客管理", size=14), - ft.Text("4: 商品管理", size=14), - ]), - padding=10, - bgcolor=ft.Colors.GREY_100, - border_radius=10 - ) - - self.controls = [ - self.title, - ft.Divider(), - self.stats_row1, - self.stats_row2, - ft.Divider(), - self.shortcuts - ] - - # データ読み込み - self.load_stats() - - def load_stats(self): - """統計データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各テーブルの件数取得 - cursor.execute("SELECT COUNT(*) FROM customers") - customers = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*) FROM products") - products = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*), COALESCE(SUM(total_price), 0) FROM sales") - sales_result = cursor.fetchone() - sales_count = sales_result[0] - total_sales = sales_result[1] - - conn.close() - - # カードを更新 - self.stats_row1.controls = [ - self._stat_card("総顧客数", customers, ft.Colors.BLUE), - self._stat_card("総商品数", products, ft.Colors.GREEN), - ] - - self.stats_row2.controls = [ - self._stat_card("総売上件数", sales_count, ft.Colors.ORANGE), - self._stat_card("総売上高", f"¥{total_sales:,.0f}", ft.Colors.PURPLE), - ] - - self.update() - - except Exception as e: - ErrorHandler.handle_error(e, "統計データ取得エラー") - - def _stat_card(self, title: str, value: str, color: ft.Colors): - """統計カード作成""" - return ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Text(title, size=16, color=color), - ft.Text(value, size=20, weight=ft.FontWeight.BOLD), - ]), - padding=15 - ), - width=200 - ) - -class SalesView(ft.Column): - """売上管理ビュー""" - - def __init__(self, page: ft.Page): - super().__init__(expand=True, spacing=15, visible=False) - # pageプロパティを設定 - object.__setattr__(self, 'page', page) - - # UI部品 - self.title = ft.Text("売上管理", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - self.customer_tf = ft.TextField(label="顧客名", width=200, autofocus=True) - self.product_tf = ft.TextField(label="商品名", width=200) - self.amount_tf = ft.TextField(label="金額", width=150, keyboard_type=ft.KeyboardType.NUMBER) - self.add_btn = ft.Button("追加", on_click=self.add_sale, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 売上一覧 - self.sales_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300) - - self.controls = [ - self.title, - ft.Divider(), - ft.Row([self.customer_tf, self.product_tf, self.amount_tf, self.add_btn]), - ft.Divider(), - ft.Text("売上一覧", size=18), - ft.Text("キーボード操作: TABでフォーカス移動, SPACE/ENTERで追加", size=12, color=ft.Colors.GREY_600), - self.sales_list - ] - - # データ読み込み - self.load_sales() - - # キーボードショートカット設定 - self._setup_keyboard_shortcuts() - - def load_sales(self): - """売上データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT customer_name, product_name, total_price, date - FROM sales - ORDER BY created_at DESC - LIMIT 20 - ''') - sales = cursor.fetchall() - conn.close() - - # リストを更新 - self.sales_list.controls.clear() - for sale in sales: - customer, product, amount, date = sale - self.sales_list.controls.append( - ft.Text(f"{date}: {customer} - {product}: ¥{amount:,.0f}") - ) - - self.update() - - except Exception as e: - ErrorHandler.handle_error(e, "売上データ読み込みエラー") - - def add_sale(self, e): - """売上データ追加""" - if self.customer_tf.value and self.product_tf.value and self.amount_tf.value: - try: - # 保存前に値を取得 - customer_val = self.customer_tf.value - product_val = self.product_tf.value - amount_val = float(self.amount_tf.value) - - # データベースに保存 - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?) - ''', (customer_val, product_val, 1, amount_val, amount_val, datetime.now().strftime("%Y-%m-%d"))) - - conn.commit() - conn.close() - - # フィールドをクリア - self.customer_tf.value = "" - self.product_tf.value = "" - self.amount_tf.value = "" - - # リスト更新 - self.load_sales() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "保存しました", ft.Colors.GREEN) - - logging.info(f"売上データ追加: {customer_val} {product_val} {amount_val}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "売上データ保存エラー") - -class CustomerView(ft.Column): - """顧客管理ビュー""" - - def __init__(self, page: ft.Page): - super().__init__(expand=True, spacing=15, visible=False) - # pageプロパティを設定 - object.__setattr__(self, 'page', page) - - # UI部品 - self.title = ft.Text("顧客管理", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # 登録フォーム - self.name_tf = ft.TextField(label="顧客名", width=200, autofocus=True) - self.phone_tf = ft.TextField(label="電話番号", width=200) - self.email_tf = ft.TextField(label="メールアドレス", width=250) - self.address_tf = ft.TextField(label="住所", width=300) - - self.add_btn = ft.Button("新規追加", on_click=self.add_customer, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE) - self.import_btn = ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 顧客一覧 - self.customer_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=200) - - self.controls = [ - self.title, - ft.Divider(), - ft.Text("新規登録", size=18, weight=ft.FontWeight.BOLD), - ft.Row([self.name_tf, self.phone_tf], spacing=10), - ft.Row([self.email_tf, self.address_tf], spacing=10), - ft.Row([self.add_btn, self.import_btn], spacing=10), - ft.Divider(), - ft.Text("顧客一覧", size=18), - ft.Text("キーボード操作: TABでフォーカス移動, SPACE/ENTERで追加", size=12, color=ft.Colors.GREY_600), - self.customer_list - ] - - # データ読み込み - self.load_customers() - - # キーボードショートカット設定 - self._setup_keyboard_shortcuts() - - def add_customer(self, e): - """顧客データ追加""" - if self.name_tf.value: - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO customers (name, phone, email, address) - VALUES (?, ?, ?, ?) - ''', (self.name_tf.value, self.phone_tf.value, self.email_tf.value, self.address_tf.value)) - - conn.commit() - conn.close() - - # フィールドをクリア - self.name_tf.value = "" - self.phone_tf.value = "" - self.email_tf.value = "" - self.address_tf.value = "" - - # リスト更新 - self.load_customers() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "顧客を追加しました", ft.Colors.GREEN) - - logging.info(f"顧客データ追加: {self.name_tf.value}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "顧客データ保存エラー") - - def load_customers(self): - """顧客データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT name, phone, email - FROM customers - ORDER BY name - LIMIT 20 - ''') - customers = cursor.fetchall() - conn.close() - - # リストを更新 - self.customer_list.controls.clear() - for customer in customers: - name, phone, email = customer - self.customer_list.controls.append( - ft.Container( - content=ft.Column([ - ft.Text(f"氏名: {name}", weight=ft.FontWeight.BOLD), - ft.Text(f"電話: {phone}", size=12), - ft.Text(f"メール: {email}", size=12), - ]), - padding=10, - bgcolor=ft.Colors.GREY_50, - border_radius=5, - margin=ft.margin.only(bottom=5) - ) - ) - - self.update() - - except Exception as e: - ErrorHandler.handle_error(e, "顧客データ読み込みエラー") - -class ProductView(ft.Column): - """商品管理ビュー""" - - def __init__(self, page: ft.Page): - super().__init__(expand=True, spacing=15, visible=False) - # pageプロパティを設定 - object.__setattr__(self, 'page', page) - - # UI部品 - self.title = ft.Text("商品管理", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # 登録フォーム - self.name_tf = ft.TextField(label="商品名", width=200, autofocus=True) - self.category_tf = ft.TextField(label="カテゴリ", width=150) - self.price_tf = ft.TextField(label="価格", width=100, keyboard_type=ft.KeyboardType.NUMBER) - self.stock_tf = ft.TextField(label="在庫数", width=100, keyboard_type=ft.KeyboardType.NUMBER) - - self.add_btn = ft.Button("新規追加", on_click=self.add_product, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE) - self.import_btn = ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 商品一覧 - self.product_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=200) - - self.controls = [ - self.title, - ft.Divider(), - ft.Text("新規登録", size=18, weight=ft.FontWeight.BOLD), - ft.Row([self.name_tf, self.category_tf, self.price_tf, self.stock_tf], spacing=10), - ft.Row([self.add_btn, self.import_btn], spacing=10), - ft.Divider(), - ft.Text("商品一覧", size=18), - ft.Text("キーボード操作: TABでフォーカス移動, SPACE/ENTERで追加", size=12, color=ft.Colors.GREY_600), - self.product_list - ] - - # データ読み込み - self.load_products() - - # キーボードショートカット設定 - self._setup_keyboard_shortcuts() - - def add_product(self, e): - """商品データ追加""" - if self.name_tf.value and self.price_tf.value: - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO products (name, category, price, stock) - VALUES (?, ?, ?, ?) - ''', (self.name_tf.value, self.category_tf.value, float(self.price_tf.value), - int(self.stock_tf.value) if self.stock_tf.value else 0)) - - conn.commit() - conn.close() - - # フィールドをクリア - self.name_tf.value = "" - self.category_tf.value = "" - self.price_tf.value = "" - self.stock_tf.value = "" - - # リスト更新 - self.load_products() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "商品を追加しました", ft.Colors.GREEN) - - logging.info(f"商品データ追加: {self.name_tf.value}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "商品データ保存エラー") - - def load_products(self): - """商品データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT name, category, price, stock - FROM products - ORDER BY name - LIMIT 20 - ''') - products = cursor.fetchall() - conn.close() - - # リストを更新 - self.product_list.controls.clear() - for product in products: - name, category, price, stock = product - self.product_list.controls.append( - ft.Container( - content=ft.Column([ - ft.Text(f"商品名: {name}", weight=ft.FontWeight.BOLD), - ft.Text(f"カテゴリ: {category}", size=12), - ft.Text(f"価格: ¥{price:,}", size=12), - ft.Text(f"在庫: {stock}個", size=12), - ]), - padding=10, - bgcolor=ft.Colors.GREY_50, - border_radius=5, - margin=ft.margin.only(bottom=5) - ) - ) - - self.update() - - except Exception as e: - ErrorHandler.handle_error(e, "商品データ読み込みエラー") - -class SalesAssistantApp: - """メインアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - ErrorHandler.current_page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) - - # データベース初期化 - self._init_database() - self._generate_dummy_data() - - # ビュー作成 - self.dashboard_view = DashboardView(page) - self.sales_view = SalesView(page) - self.customer_view = CustomerView(page) - self.product_view = ProductView(page) - - # ナビゲーション設定 - self._build_navigation() - - # キーボードショートカット設定 - self._setup_keyboard_shortcuts() - - # 初期表示 - self.dashboard_view.visible = True - self.sales_view.visible = False - self.customer_view.visible = False - self.product_view.visible = False - - logging.info("アプリケーション起動完了") - print("🚀 Compiz対応販売アシスト1号起動完了") - - def _signal_handler(self, signum, frame): - """シグナルハンドラ""" - print(f"\nシグナル {signum} を受信しました") - self._cleanup_resources() - sys.exit(0) - - def _cleanup_resources(self): - """リソースクリーンアップ""" - try: - logging.info("アプリケーション終了処理開始") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - except Exception as e: - logging.error(f"クリーンアップエラー: {e}") - print(f"❌ クリーンアップエラー: {e}") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各テーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_name TEXT NOT NULL, - product_name TEXT NOT NULL, - quantity INTEGER NOT NULL, - unit_price REAL NOT NULL, - total_price REAL NOT NULL, - date TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("データベース初期化完了") - - except Exception as e: - ErrorHandler.handle_error(e, "データベース初期化エラー") - - def _generate_dummy_data(self): - """ダミーデータ生成""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 既存データチェック - cursor.execute("SELECT COUNT(*) FROM customers") - if cursor.fetchone()[0] == 0: - print("📊 ダミーデータを生成中...") - - # 顧客データ - customers = DummyDataGenerator.generate_customers(50) - for customer in customers: - cursor.execute(''' - INSERT INTO customers (name, phone, email, address) - VALUES (?, ?, ?, ?) - ''', (customer['name'], customer['phone'], customer['email'], customer['address'])) - - # 商品データ - products = DummyDataGenerator.generate_products(30) - for product in products: - cursor.execute(''' - INSERT INTO products (name, category, price, stock) - VALUES (?, ?, ?, ?) - ''', (product['name'], product['category'], product['price'], product['stock'])) - - # 売上データ - sales = DummyDataGenerator.generate_sales(100) - for sale in sales: - cursor.execute(''' - INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?) - ''', (sale['customer_name'], sale['product_name'], sale['quantity'], - sale['unit_price'], sale['total_price'], sale['date'])) - - conn.commit() - print("✅ ダミーデータ生成完了") - - conn.close() - - except Exception as e: - ErrorHandler.handle_error(e, "ダミーデータ生成エラー") - - def _build_navigation(self): - """ナビゲーション構築""" - try: - # NavigationBar(Compiz対応) - self.page.navigation_bar = ft.NavigationBar( - selected_index=0, # 最初はダッシュボード - destinations=[ - ft.NavigationBarDestination( - icon=ft.Icons.DASHBOARD, - label="ダッシュボード" - ), - ft.NavigationBarDestination( - icon=ft.Icons.SHOPPING_CART, - label="売上" - ), - ft.NavigationBarDestination( - icon=ft.Icons.PEOPLE, - label="顧客" - ), - ft.NavigationBarDestination( - icon=ft.Icons.INVENTORY, - label="商品" - ) - ], - on_change=self._on_nav_change - ) - - # 全てのビューをページに追加 - self.page.add( - ft.Container( - content=ft.Column([ - self.dashboard_view, - self.sales_view, - self.customer_view, - self.product_view - ], expand=True), - padding=10, - expand=True - ) - ) - - except Exception as e: - ErrorHandler.handle_error(e, "ナビゲーション構築エラー") - - def _on_nav_change(self, e): - """ナビゲーション変更イベント""" - try: - index = e.control.selected_index - - # 全てのビューを非表示 - self.dashboard_view.visible = False - self.sales_view.visible = False - self.customer_view.visible = False - self.product_view.visible = False - - # 選択されたビューを表示 - if index == 0: - self.dashboard_view.visible = True - self.dashboard_view.load_stats() # データ更新 - elif index == 1: - self.sales_view.visible = True - self.sales_view.load_sales() # データ更新 - elif index == 2: - self.customer_view.visible = True - self.customer_view.load_customers() # データ更新 - elif index == 3: - self.product_view.visible = True - self.product_view.load_products() # データ更新 - - self.page.update() - logging.info(f"ページ遷移: index={index}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "ナビゲーション変更エラー") - - def _setup_keyboard_shortcuts(self): - """キーボードショートカット設定""" - try: - def on_keyboard(e: ft.KeyboardEvent): - # SPACEまたはENTERで追加 - if e.key == " " or e.key == "Enter": - if self.sales_view.visible: - self.sales_view.add_sale(None) - elif self.customer_view.visible: - self.customer_view.add_customer(None) - elif self.product_view.visible: - self.product_view.add_product(None) - # 数字キーでページ遷移 - elif e.key == "1": - self.page.navigation_bar.selected_index = 0 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - elif e.key == "2": - self.page.navigation_bar.selected_index = 1 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - elif e.key == "3": - self.page.navigation_bar.selected_index = 2 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - elif e.key == "4": - self.page.navigation_bar.selected_index = 3 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - # ESCでダッシュボードに戻る - elif e.key == "Escape": - self.page.navigation_bar.selected_index = 0 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - - self.page.on_keyboard_event = on_keyboard - logging.info("キーボードショートカット設定完了") - - except Exception as e: - ErrorHandler.handle_error(e, "キーボードショートカット設定エラー") - - def _setup_keyboard_shortcuts(self): - """各ビューのキーボードショートカット設定(ダミー)""" - pass - -def main(page: ft.Page): - """メイン関数""" - try: - # ウィンドウ設定 - page.title = "販売アシスト1号 (Compiz対応)" - page.window_width = 800 - page.window_height = 600 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: SalesAssistantApp(page)._cleanup_resources() - - # アプリケーション起動 - app = SalesAssistantApp(page) - - except Exception as e: - ErrorHandler.handle_error(e, "アプリケーション起動エラー") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_compiz_fixed_v2.py b/app_compiz_fixed_v2.py deleted file mode 100644 index ef55140..0000000 --- a/app_compiz_fixed_v2.py +++ /dev/null @@ -1,837 +0,0 @@ -import flet as ft -import sqlite3 -import signal -import sys -import logging -import random -from datetime import datetime -from typing import List, Dict, Optional - -class ErrorHandler: - """グローバルエラーハンドラ""" - - @staticmethod - def handle_error(error: Exception, context: str = ""): - """エラーを一元処理""" - error_msg = f"{context}: {str(error)}" - logging.error(error_msg) - print(f"❌ {error_msg}") - - try: - if hasattr(ErrorHandler, 'current_page'): - ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) - except: - pass - - @staticmethod - def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): - """SnackBarを表示""" - try: - page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - page.snack_bar.open = True - page.update() - except: - pass - -class DummyDataGenerator: - """テスト用ダミーデータ生成""" - - @staticmethod - def generate_customers(count: int = 100) -> List[Dict]: - """ダミー顧客データ生成""" - first_names = ["田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本", "中村", "小林", "加藤"] - last_names = ["太郎", "次郎", "三郎", "花子", "美子", "健一", "恵子", "大輔", "由美", "翔太"] - - customers = [] - for i in range(count): - name = f"{random.choice(first_names)} {random.choice(last_names)}" - customers.append({ - 'id': i + 1, - 'name': name, - 'phone': f"090-{random.randint(1000, 9999)}-{random.randint(1000, 9999)}", - 'email': f"customer{i+1}@example.com", - 'address': f"東京都{random.choice(['渋谷区', '新宿区', '港区', '千代田区'])}{random.randint(1, 50)}-{random.randint(1, 10)}" - }) - return customers - - @staticmethod - def generate_products(count: int = 50) -> List[Dict]: - """ダミー商品データ生成""" - categories = ["電子機器", "衣料品", "食品", "書籍", "家具"] - products = [] - - for i in range(count): - category = random.choice(categories) - products.append({ - 'id': i + 1, - 'name': f"{category}{i+1}", - 'category': category, - 'price': random.randint(100, 50000), - 'stock': random.randint(0, 100) - }) - return products - - @staticmethod - def generate_sales(count: int = 200) -> List[Dict]: - """ダミー売上データ生成""" - customers = DummyDataGenerator.generate_customers(20) - products = DummyDataGenerator.generate_products(30) - - sales = [] - for i in range(count): - customer = random.choice(customers) - product = random.choice(products) - quantity = random.randint(1, 10) - - sales.append({ - 'id': i + 1, - 'customer_id': customer['id'], - 'customer_name': customer['name'], - 'product_id': product['id'], - 'product_name': product['name'], - 'quantity': quantity, - 'unit_price': product['price'], - 'total_price': quantity * product['price'], - 'date': datetime.now().strftime("%Y-%m-%d"), - 'created_at': datetime.now().isoformat() - }) - return sales - -class DashboardView: - """ダッシュボードビュー""" - - def __init__(self, page: ft.Page): - self._page = None - - # UI構築 - self.title = ft.Text("ダッシュボード", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # 統計カード - self.stats_row1 = ft.Row([], spacing=10) - self.stats_row2 = ft.Row([], spacing=10) - - # キーボードショートカット表示 - self.shortcuts = ft.Container( - content=ft.Column([ - ft.Text("キーボードショートカット", size=16, weight=ft.FontWeight.BOLD), - ft.Text("1: ダッシュボード", size=14), - ft.Text("2: 売上管理", size=14), - ft.Text("3: 顧客管理", size=14), - ft.Text("4: 商品管理", size=14), - ft.Text("ESC: ダッシュボードに戻る", size=14), - ]), - padding=10, - bgcolor=ft.Colors.GREY_100, - border_radius=10 - ) - - # データ読み込み - self.load_stats() - - @property - def page(self): - return self._page - - @page.setter - def page(self, value): - self._page = value - - def build(self): - """UIを構築して返す""" - return ft.Column([ - self.title, - ft.Divider(), - self.stats_row1, - self.stats_row2, - ft.Divider(), - self.shortcuts - ], expand=True, spacing=15) - - def load_stats(self): - """統計データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各テーブルの件数取得 - cursor.execute("SELECT COUNT(*) FROM customers") - customers = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*) FROM products") - products = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*), COALESCE(SUM(total_price), 0) FROM sales") - sales_result = cursor.fetchone() - sales_count = sales_result[0] - total_sales = sales_result[1] - - conn.close() - - # カードを更新 - self.stats_row1.controls = [ - self._stat_card("総顧客数", customers, ft.Colors.BLUE), - self._stat_card("総商品数", products, ft.Colors.GREEN), - ] - - self.stats_row2.controls = [ - self._stat_card("総売上件数", sales_count, ft.Colors.ORANGE), - self._stat_card("総売上高", f"¥{total_sales:,.0f}", ft.Colors.PURPLE), - ] - - except Exception as e: - ErrorHandler.handle_error(e, "統計データ取得エラー") - - def _stat_card(self, title: str, value: str, color: ft.Colors): - """統計カード作成""" - return ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Text(title, size=16, color=color), - ft.Text(value, size=20, weight=ft.FontWeight.BOLD), - ]), - padding=15 - ), - width=200 - ) - -class SalesView: - """売上管理ビュー""" - - def __init__(self, page: ft.Page): - self._page = None - - # UI部品 - self.title = ft.Text("売上管理", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - self.customer_tf = ft.TextField(label="顧客名", width=200, autofocus=True) - self.product_tf = ft.TextField(label="商品名", width=200) - self.amount_tf = ft.TextField(label="金額", width=150, keyboard_type=ft.KeyboardType.NUMBER) - self.add_btn = ft.Button("追加", on_click=self.add_sale, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 売上一覧 - self.sales_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300) - - # データ読み込み - self.load_sales() - - @property - def page(self): - return self._page - - @page.setter - def page(self, value): - self._page = value - - def build(self): - """UIを構築して返す""" - return ft.Column([ - self.title, - ft.Divider(), - ft.Row([self.customer_tf, self.product_tf, self.amount_tf, self.add_btn]), - ft.Divider(), - ft.Text("売上一覧", size=18), - ft.Text("キーボード操作: TABでフォーカス移動, SPACE/ENTERで追加", size=12, color=ft.Colors.GREY_600), - self.sales_list - ], expand=True, spacing=15) - - def load_sales(self): - """売上データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT customer_name, product_name, total_price, date - FROM sales - ORDER BY created_at DESC - LIMIT 20 - ''') - sales = cursor.fetchall() - conn.close() - - # リストを更新 - self.sales_list.controls.clear() - for sale in sales: - customer, product, amount, date = sale - self.sales_list.controls.append( - ft.Text(f"{date}: {customer} - {product}: ¥{amount:,.0f}") - ) - - except Exception as e: - ErrorHandler.handle_error(e, "売上データ読み込みエラー") - - def add_sale(self, e): - """売上データ追加""" - if self.customer_tf.value and self.product_tf.value and self.amount_tf.value: - try: - # 保存前に値を取得 - customer_val = self.customer_tf.value - product_val = self.product_tf.value - amount_val = float(self.amount_tf.value) - - # データベースに保存 - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?) - ''', (customer_val, product_val, 1, amount_val, amount_val, datetime.now().strftime("%Y-%m-%d"))) - - conn.commit() - conn.close() - - # フィールドをクリア - self.customer_tf.value = "" - self.product_tf.value = "" - self.amount_tf.value = "" - - # リスト更新 - self.load_sales() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "保存しました", ft.Colors.GREEN) - - logging.info(f"売上データ追加: {customer_val} {product_val} {amount_val}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "売上データ保存エラー") - -class CustomerView: - """顧客管理ビュー""" - - def __init__(self, page: ft.Page): - self._page = None - - # UI部品 - self.title = ft.Text("顧客管理", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # 登録フォーム - self.name_tf = ft.TextField(label="顧客名", width=200, autofocus=True) - self.phone_tf = ft.TextField(label="電話番号", width=200) - self.email_tf = ft.TextField(label="メールアドレス", width=250) - self.address_tf = ft.TextField(label="住所", width=300) - - self.add_btn = ft.Button("新規追加", on_click=self.add_customer, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE) - self.import_btn = ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 顧客一覧 - self.customer_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=250) - - # データ読み込み - self.load_customers() - - @property - def page(self): - return self._page - - @page.setter - def page(self, value): - self._page = value - - def build(self): - """UIを構築して返す""" - return ft.Column([ - self.title, - ft.Divider(), - ft.Text("新規登録", size=18, weight=ft.FontWeight.BOLD), - ft.Row([self.name_tf, self.phone_tf], spacing=10), - ft.Row([self.email_tf, self.address_tf], spacing=10), - ft.Row([self.add_btn, self.import_btn], spacing=10), - ft.Divider(), - ft.Text("顧客一覧", size=18), - ft.Text("キーボード操作: TABでフォーカス移動, SPACE/ENTERで追加", size=12, color=ft.Colors.GREY_600), - self.customer_list - ], expand=True, spacing=15) - - def add_customer(self, e): - """顧客データ追加""" - if self.name_tf.value: - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO customers (name, phone, email, address) - VALUES (?, ?, ?, ?) - ''', (self.name_tf.value, self.phone_tf.value, self.email_tf.value, self.address_tf.value)) - - conn.commit() - conn.close() - - # フィールドをクリア - self.name_tf.value = "" - self.phone_tf.value = "" - self.email_tf.value = "" - self.address_tf.value = "" - - # リスト更新 - self.load_customers() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "顧客を追加しました", ft.Colors.GREEN) - - logging.info(f"顧客データ追加: {self.name_tf.value}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "顧客データ保存エラー") - - def load_customers(self): - """顧客データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT name, phone, email - FROM customers - ORDER BY name - LIMIT 20 - ''') - customers = cursor.fetchall() - conn.close() - - # リストを更新 - self.customer_list.controls.clear() - for customer in customers: - name, phone, email = customer - self.customer_list.controls.append( - ft.Container( - content=ft.Column([ - ft.Text(f"氏名: {name}", weight=ft.FontWeight.BOLD), - ft.Text(f"電話: {phone}", size=12), - ft.Text(f"メール: {email}", size=12), - ]), - padding=10, - bgcolor=ft.Colors.GREY_50, - border_radius=5, - margin=ft.margin.only(bottom=5) - ) - ) - - except Exception as e: - ErrorHandler.handle_error(e, "顧客データ読み込みエラー") - -class ProductView: - """商品管理ビュー""" - - def __init__(self, page: ft.Page): - self._page = None - - # UI部品 - self.title = ft.Text("商品管理", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # 登録フォーム - self.name_tf = ft.TextField(label="商品名", width=200, autofocus=True) - self.category_tf = ft.TextField(label="カテゴリ", width=150) - self.price_tf = ft.TextField(label="価格", width=100, keyboard_type=ft.KeyboardType.NUMBER) - self.stock_tf = ft.TextField(label="在庫数", width=100, keyboard_type=ft.KeyboardType.NUMBER) - - self.add_btn = ft.Button("新規追加", on_click=self.add_product, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE) - self.import_btn = ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 商品一覧 - self.product_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=250) - - # データ読み込み - self.load_products() - - @property - def page(self): - return self._page - - @page.setter - def page(self, value): - self._page = value - - def build(self): - """UIを構築して返す""" - return ft.Column([ - self.title, - ft.Divider(), - ft.Text("新規登録", size=18, weight=ft.FontWeight.BOLD), - ft.Row([self.name_tf, self.category_tf, self.price_tf, self.stock_tf], spacing=10), - ft.Row([self.add_btn, self.import_btn], spacing=10), - ft.Divider(), - ft.Text("商品一覧", size=18), - ft.Text("キーボード操作: TABでフォーカス移動, SPACE/ENTERで追加", size=12, color=ft.Colors.GREY_600), - self.product_list - ], expand=True, spacing=15) - - def add_product(self, e): - """商品データ追加""" - if self.name_tf.value and self.price_tf.value: - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO products (name, category, price, stock) - VALUES (?, ?, ?, ?) - ''', (self.name_tf.value, self.category_tf.value, float(self.price_tf.value), - int(self.stock_tf.value) if self.stock_tf.value else 0)) - - conn.commit() - conn.close() - - # フィールドをクリア - self.name_tf.value = "" - self.category_tf.value = "" - self.price_tf.value = "" - self.stock_tf.value = "" - - # リスト更新 - self.load_products() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "商品を追加しました", ft.Colors.GREEN) - - logging.info(f"商品データ追加: {self.name_tf.value}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "商品データ保存エラー") - - def load_products(self): - """商品データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT name, category, price, stock - FROM products - ORDER BY name - LIMIT 20 - ''') - products = cursor.fetchall() - conn.close() - - # リストを更新 - self.product_list.controls.clear() - for product in products: - name, category, price, stock = product - self.product_list.controls.append( - ft.Container( - content=ft.Column([ - ft.Text(f"商品名: {name}", weight=ft.FontWeight.BOLD), - ft.Text(f"カテゴリ: {category}", size=12), - ft.Text(f"価格: ¥{price:,}", size=12), - ft.Text(f"在庫: {stock}個", size=12), - ]), - padding=10, - bgcolor=ft.Colors.GREY_50, - border_radius=5, - margin=ft.margin.only(bottom=5) - ) - ) - - except Exception as e: - ErrorHandler.handle_error(e, "商品データ読み込みエラー") - -class SalesAssistantApp: - """メインアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - ErrorHandler.current_page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) - - # データベース初期化 - self._init_database() - self._generate_dummy_data() - - # ビュー作成 - self.dashboard_view = DashboardView(page) - self.sales_view = SalesView(page) - self.customer_view = CustomerView(page) - self.product_view = ProductView(page) - - # ナビゲーション設定 - self._build_navigation() - - # キーボードショートカット設定 - self._setup_keyboard_shortcuts() - - # 初期表示 - self.dashboard_container.visible = True - self.sales_container.visible = False - self.customer_container.visible = False - self.product_container.visible = False - - logging.info("アプリケーション起動完了") - print("🚀 Compiz対応販売アシスト1号起動完了") - - def _signal_handler(self, signum, frame): - """シグナルハンドラ""" - print(f"\nシグナル {signum} を受信しました") - self._cleanup_resources() - sys.exit(0) - - def _cleanup_resources(self): - """リソースクリーンアップ""" - try: - logging.info("アプリケーション終了処理開始") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - except Exception as e: - logging.error(f"クリーンアップエラー: {e}") - print(f"❌ クリーンアップエラー: {e}") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各テーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_name TEXT NOT NULL, - product_name TEXT NOT NULL, - quantity INTEGER NOT NULL, - unit_price REAL NOT NULL, - total_price REAL NOT NULL, - date TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("データベース初期化完了") - - except Exception as e: - ErrorHandler.handle_error(e, "データベース初期化エラー") - - def _generate_dummy_data(self): - """ダミーデータ生成""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 既存データチェック - cursor.execute("SELECT COUNT(*) FROM customers") - if cursor.fetchone()[0] == 0: - print("📊 ダミーデータを生成中...") - - # 顧客データ - customers = DummyDataGenerator.generate_customers(50) - for customer in customers: - cursor.execute(''' - INSERT INTO customers (name, phone, email, address) - VALUES (?, ?, ?, ?) - ''', (customer['name'], customer['phone'], customer['email'], customer['address'])) - - # 商品データ - products = DummyDataGenerator.generate_products(30) - for product in products: - cursor.execute(''' - INSERT INTO products (name, category, price, stock) - VALUES (?, ?, ?, ?) - ''', (product['name'], product['category'], product['price'], product['stock'])) - - # 売上データ - sales = DummyDataGenerator.generate_sales(100) - for sale in sales: - cursor.execute(''' - INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?) - ''', (sale['customer_name'], sale['product_name'], sale['quantity'], - sale['unit_price'], sale['total_price'], sale['date'])) - - conn.commit() - print("✅ ダミーデータ生成完了") - - conn.close() - - except Exception as e: - ErrorHandler.handle_error(e, "ダミーデータ生成エラー") - - def _build_navigation(self): - """ナビゲーション構築""" - try: - # NavigationBar(Compiz対応) - self.page.navigation_bar = ft.NavigationBar( - selected_index=0, # 最初はダッシュボード - destinations=[ - ft.NavigationBarDestination( - icon=ft.Icons.DASHBOARD, - label="ダッシュボード" - ), - ft.NavigationBarDestination( - icon=ft.Icons.SHOPPING_CART, - label="売上" - ), - ft.NavigationBarDestination( - icon=ft.Icons.PEOPLE, - label="顧客" - ), - ft.NavigationBarDestination( - icon=ft.Icons.INVENTORY, - label="商品" - ) - ], - on_change=self._on_nav_change - ) - - # コンテナ作成 - self.dashboard_container = ft.Container( - content=self.dashboard_view.build(), - visible=True - ) - self.sales_container = ft.Container( - content=self.sales_view.build(), - visible=False - ) - self.customer_container = ft.Container( - content=self.customer_view.build(), - visible=False - ) - self.product_container = ft.Container( - content=self.product_view.build(), - visible=False - ) - - # 全てのコンテナをページに追加 - self.page.add( - ft.Container( - content=ft.Column([ - self.dashboard_container, - self.sales_container, - self.customer_container, - self.product_container - ], expand=True), - padding=10, - expand=True - ) - ) - - except Exception as e: - ErrorHandler.handle_error(e, "ナビゲーション構築エラー") - - def _on_nav_change(self, e): - """ナビゲーション変更イベント""" - try: - index = e.control.selected_index - - # 全てのコンテナを非表示 - self.dashboard_container.visible = False - self.sales_container.visible = False - self.customer_container.visible = False - self.product_container.visible = False - - # 選択されたコンテナを表示 - if index == 0: - self.dashboard_container.visible = True - self.dashboard_view.load_stats() # データ更新 - elif index == 1: - self.sales_container.visible = True - self.sales_view.load_sales() # データ更新 - elif index == 2: - self.customer_container.visible = True - self.customer_view.load_customers() # データ更新 - elif index == 3: - self.product_container.visible = True - self.product_view.load_products() # データ更新 - - self.page.update() - logging.info(f"ページ遷移: index={index}") - - except Exception as ex: - ErrorHandler.handle_error(ex, "ナビゲーション変更エラー") - - def _setup_keyboard_shortcuts(self): - """キーボードショートカット設定""" - try: - def on_keyboard(e: ft.KeyboardEvent): - # SPACEまたはENTERで追加 - if e.key == " " or e.key == "Enter": - if self.sales_container.visible: - self.sales_view.add_sale(None) - elif self.customer_container.visible: - self.customer_view.add_customer(None) - elif self.product_container.visible: - self.product_view.add_product(None) - # 数字キーでページ遷移 - elif e.key == "1": - self.page.navigation_bar.selected_index = 0 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - elif e.key == "2": - self.page.navigation_bar.selected_index = 1 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - elif e.key == "3": - self.page.navigation_bar.selected_index = 2 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - elif e.key == "4": - self.page.navigation_bar.selected_index = 3 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - # ESCでダッシュボードに戻る - elif e.key == "Escape": - self.page.navigation_bar.selected_index = 0 - self._on_nav_change(ft.ControlEvent(control=self.page.navigation_bar)) - - self.page.on_keyboard_event = on_keyboard - logging.info("キーボードショートカット設定完了") - - except Exception as e: - ErrorHandler.handle_error(e, "キーボードショートカット設定エラー") - -def main(page: ft.Page): - """メイン関数""" - try: - # ウィンドウ設定 - page.title = "販売アシスト1号 (Compiz対応)" - page.window_width = 800 - page.window_height = 600 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: SalesAssistantApp(page)._cleanup_resources() - - # アプリケーション起動 - app = SalesAssistantApp(page) - - except Exception as e: - ErrorHandler.handle_error(e, "アプリケーション起動エラー") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_compiz_shortcuts.py b/app_compiz_shortcuts.py deleted file mode 100644 index a553e67..0000000 --- a/app_compiz_shortcuts.py +++ /dev/null @@ -1,395 +0,0 @@ -""" -Compiz対応ショートカットキー画面 -Mate+Compiz環境で安定動作するUI -""" - -import flet as ft -import signal -import sys -import logging -from datetime import datetime - -class CompizShortcutsApp: - """Compiz対応ショートカットキーアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - self.signal_handler = signal_handler # インスタンス変数として保存 - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # ウィンドウ設定 - スマホ固定サイズ - 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) - - # メインコンテナ - self.main_container = ft.Column([], expand=True, spacing=20) - - # ショートカットキー設定 - self.setup_shortcuts() - - # UI構築 - self.build_ui() - - logging.info("Compiz対応ショートカットキーアプリ起動完了") - print("🚀 Compiz対応ショートカットキーアプリ起動完了") - - def setup_shortcuts(self): - """ショートカットキー設定""" - self.key_pressed = {} # キー押下状態を管理 - self.last_key_time = {} # 最後のキー押下時間を管理 - - def on_keyboard(e: ft.KeyboardEvent): - import time - current_time = time.time() - - # キーリピート防止(0.2秒以内の同じキーを無視) - if e.key in self.last_key_time and current_time - self.last_key_time[e.key] < 0.2: - return - - self.last_key_time[e.key] = current_time - - # 数字キーで機能呼び出し - if e.key == "1": - self.show_function("ダッシュボード", "統計情報の表示") - elif e.key == "2": - self.show_function("売上管理", "売上データの入力・管理") - elif e.key == "3": - self.show_function("顧客管理", "顧客マスタの編集") - elif e.key == "4": - self.show_function("商品管理", "商品マスタの編集") - elif e.key == "5": - self.show_function("伝票入力", "伝票データの入力") - elif e.key == "6": - self.show_function("テキストエディタ", "下書き・メモの作成") - elif e.key == "7": - self.show_function("GPS機能", "GPS情報の取得・管理") - elif e.key == "8": - self.show_function("PDF出力", "帳票のPDF出力") - elif e.key == "9": - self.show_function("設定", "アプリケーション設定") - elif e.key == "0": - self.show_function("終了", "アプリケーションの終了") - # ESCキーで終了 - elif e.key == "Escape": - self.show_function("終了", "アプリケーションの終了") - # SPACEで実行 - elif e.key == " ": - self.execute_current_function() - # ENTERで実行 - elif e.key == "Enter": - self.execute_current_function() - - self.page.on_keyboard_event = on_keyboard - logging.info("ショートカットキー設定完了") - - def build_ui(self): - """UIを構築""" - # タイトル - self.title = ft.Text( - "Compiz対応ショートカットキー", - size=36, - weight=ft.FontWeight.BOLD, - color=ft.Colors.BLUE_900, - text_align=ft.TextAlign.CENTER - ) - - # 説明テキスト - self.description = ft.Text( - "Mate+Compiz環境で安定動作するショートカットキー対応画面", - size=18, - color=ft.Colors.GREY_600, - text_align=ft.TextAlign.CENTER - ) - - # 現在の機能表示 - self.current_function = ft.Container( - content=ft.Text( - "機能: 未選択", - size=24, - weight=ft.FontWeight.BOLD, - color=ft.Colors.WHITE, - text_align=ft.TextAlign.CENTER - ), - padding=20, - bgcolor=ft.Colors.ORANGE, - border_radius=15, - margin=ft.Margin.symmetric(vertical=10) - ) - - # ショートカットキーガイド - self.shortcuts_guide = ft.Container( - content=ft.Column([ - ft.Text("ショートカットキー一覧", size=20, weight=ft.FontWeight.BOLD, text_align=ft.TextAlign.CENTER), - ft.Divider(height=2, thickness=2), - self.create_shortcut_row("1", "ダッシュボード", "統計情報の表示", ft.Colors.BLUE), - self.create_shortcut_row("2", "売上管理", "売上データの入力・管理", ft.Colors.GREEN), - self.create_shortcut_row("3", "顧客管理", "顧客マスタの編集", ft.Colors.ORANGE), - self.create_shortcut_row("4", "商品管理", "階層構造商品マスター編集", ft.Colors.PURPLE), - self.create_shortcut_row("5", "伝票入力", "伝票データの入力", ft.Colors.RED), - self.create_shortcut_row("6", "テキストエディタ", "下書き・メモの作成", ft.Colors.TEAL), - self.create_shortcut_row("7", "GPS機能", "GPS情報の取得・管理", ft.Colors.CYAN), - self.create_shortcut_row("8", "PDF出力", "帳票のPDF出力", ft.Colors.BROWN), - self.create_shortcut_row("9", "設定", "アプリケーション設定", ft.Colors.GREY), - self.create_shortcut_row("0", "終了", "アプリケーションの終了", ft.Colors.RED), - ft.Divider(height=2, thickness=2), - ft.Container( - content=ft.Column([ - ft.Text("操作方法:", size=16, weight=ft.FontWeight.BOLD), - ft.Text("• 数字キー: 機能を選択", size=14), - ft.Text("• SPACE/ENTER: 選択した機能を実行", size=14), - ft.Text("• ESC: アプリケーション終了", size=14), - ft.Text("• マウスクリックも可能", size=14), - ], spacing=5), - padding=15, - bgcolor=ft.Colors.BLUE_50, - border_radius=10 - ) - ], spacing=10), - padding=20, - 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=20) - ) - - # 実行ボタン(マウス用) - self.execute_btn = ft.Container( - content=ft.Button( - "実行", - on_click=self.execute_current_function, - style=ft.ButtonStyle( - bgcolor=ft.Colors.GREEN, - color=ft.Colors.WHITE, - elevation=5, - shape=ft.RoundedRectangleBorder(radius=10), - padding=ft.Padding.symmetric(horizontal=30, vertical=15) - ) - ), - alignment=ft.alignment.Alignment(0, 0), - margin=ft.Margin.symmetric(vertical=10) - ) - - # 環境情報 - self.env_info = ft.Container( - content=ft.Column([ - ft.Text("環境情報", size=16, weight=ft.FontWeight.BOLD), - ft.Text(f"OS: Linux Mint + Compiz"), - ft.Text(f"デスクトップ環境: Mate"), - ft.Text(f"対応: Compiz特殊操作に最適化"), - ft.Text(f"作成日時: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"), - ], spacing=5), - padding=15, - bgcolor=ft.Colors.BLUE_50, - border_radius=10, - shadow=ft.BoxShadow( - spread_radius=1, - blur_radius=3, - color=ft.Colors.GREY_200, - offset=ft.Offset(0, 1) - ) - ) - - # メインコンテナに追加 - self.main_container.controls = [ - ft.Container( - content=self.title, - margin=ft.Margin.only(bottom=10) - ), - ft.Container( - content=self.description, - margin=ft.Margin.only(bottom=20) - ), - ft.Divider(height=1, thickness=1), - self.current_function, - ft.Row([ - self.execute_btn, - self.env_info - ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), - ft.Divider(height=1, thickness=1), - ft.Container( - content=self.shortcuts_guide, - expand=True - ) - ] - - # ページに追加 - self.page.add( - ft.Container( - content=self.main_container, - padding=20, - bgcolor=ft.Colors.GREY_100, - width=380 # 360px → 380pxに拡大 - ) - ) - - def create_shortcut_row(self, key: str, title: str, description: str, color: ft.Colors) -> ft.Container: - """ショートカットキー行を作成""" - return ft.Container( - content=ft.Row([ - ft.Container( - content=ft.Text(key, size=22, weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE), - width=60, - height=60, - bgcolor=color, - alignment=ft.alignment.Alignment(0, 0), - border_radius=12, - shadow=ft.BoxShadow( - spread_radius=1, - blur_radius=3, - color=ft.Colors.with_opacity(0.3, color), - offset=ft.Offset(0, 2) - ) - ), - ft.Container( - content=ft.Column([ - 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=280, # 280px → 300pxに調整 - padding=ft.Padding.symmetric(horizontal=10, vertical=8), - bgcolor=ft.Colors.GREY_50, - border_radius=10, - margin=ft.Margin.only(left=8) - ) - ], spacing=0, width=340), # Rowに幅制限を追加 - margin=ft.Margin.only(bottom=10), - on_click=lambda _: self.show_function(title, description) - ) - - def show_function(self, title: str, description: str): - """機能情報を表示""" - self.current_function.content.value = f"機能: {title}" - self.current_function.content.color = ft.Colors.WHITE - self.current_function.bgcolor = ft.Colors.BLUE_900 - self.page.update() - logging.info(f"機能選択: {title}") - - def execute_current_function(self, e=None): - """現在の機能を実行""" - current_text = self.current_function.content.value - - if "ダッシュボード" in current_text: - self.show_message("ダッシュボード機能を起動します", ft.Colors.GREEN) - # 実際のアプリ起動 - self.launch_app("app_compiz_fixed.py") - logging.info("ダッシュボード機能実行") - - elif "売上管理" in current_text: - self.show_message("売上管理機能を起動します", ft.Colors.GREEN) - self.launch_app("app_simple_working.py") - logging.info("売上管理機能実行") - - elif "顧客管理" in current_text: - self.show_message("顧客管理機能を起動します", ft.Colors.GREEN) - self.launch_app("app_master_management.py") - logging.info("顧客管理機能実行") - - elif "商品管理" in current_text: - self.show_message("商品管理機能を起動します", ft.Colors.GREEN) - self.launch_app("app_hierarchical_product_master.py") - logging.info("商品管理機能実行") - - elif "伝票入力" in current_text: - self.show_message("伝票入力機能を起動します", ft.Colors.GREEN) - self.launch_app("app_slip_framework_demo.py") - logging.info("伝票入力機能実行") - - elif "テキストエディタ" in current_text: - self.show_message("テキストエディタ機能を起動します", ft.Colors.GREEN) - self.launch_app("app_text_editor.py") - logging.info("テキストエディタ機能実行") - - elif "GPS機能" in current_text: - self.show_message("GPS機能を起動します", ft.Colors.GREEN) - self.launch_app("app_theme_master.py") - logging.info("GPS機能実行") - - elif "PDF出力" in current_text: - self.show_message("PDF出力機能を起動します", ft.Colors.GREEN) - self.launch_app("app_framework_demo.py") - logging.info("PDF出力機能実行") - - elif "設定" in current_text: - self.show_message("設定機能を起動します", ft.Colors.GREEN) - self.launch_app("app_robust.py") - logging.info("設定機能実行") - - elif "終了" in current_text: - self.show_message("アプリケーションを終了します", ft.Colors.RED) - self.signal_handler(0, None) - - def launch_app(self, app_name: str): - """アプリを起動""" - import subprocess - import os - - try: - # 現在のディレクトリでアプリを起動 - script_dir = os.path.dirname(os.path.abspath(__file__)) - app_path = os.path.join(script_dir, app_name) - - # バックグラウンドでアプリ起動 - subprocess.Popen([ - "flet", "run", app_path - ], cwd=script_dir) - - except Exception as e: - logging.error(f"アプリ起動エラー: {e}") - self.show_message(f"アプリ起動に失敗しました: {e}", ft.Colors.RED) - - def show_message(self, message: str, color: ft.Colors): - """メッセージを表示""" - self.page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - self.page.snack_bar.open = True - self.page.update() - -def main(page: ft.Page): - """メイン関数""" - try: - app = CompizShortcutsApp(page) - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_dashboard_template.py b/app_dashboard_template.py deleted file mode 100644 index b00538f..0000000 --- a/app_dashboard_template.py +++ /dev/null @@ -1,333 +0,0 @@ -""" -販売アシスト・ダッシュボード(テンプレート) -統合ハブとして全機能へのアクセスを提供 -""" - -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 deleted file mode 100644 index 1632034..0000000 --- a/app_flutter_style_dashboard.py +++ /dev/null @@ -1,1831 +0,0 @@ -""" -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, InvoiceItem -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 ZoomableContainer(ft.Container): - """ピンチイン/アウト対応コンテナ""" - - def __init__(self, content=None, min_scale=0.5, max_scale=3.0, **kwargs): - super().__init__(**kwargs) - self.content = content - self.min_scale = min_scale - self.max_scale = max_scale - self.scale = 1.0 - self.initial_distance = 0 - - # ズーム機能を無効化してシンプルなコンテナとして使用 - # 将来的な実装のためにクラスは残す - -class AppBar(ft.Container): - """標準化されたアプリケーションヘッダー""" - - def __init__(self, title: str, show_back: bool = False, show_edit: bool = False, - on_back=None, on_edit=None, page=None): - super().__init__() - self.title = title - self.show_back = show_back - self.show_edit = show_edit - self.on_back = on_back - self.on_edit = on_edit - self.page_ref = page # page_refとして保存 - - self.bgcolor = ft.Colors.BLUE_GREY_50 - self.padding = ft.Padding.symmetric(horizontal=16, vertical=8) - - self.content = self._build_content() - - def _build_content(self) -> ft.Row: - """AppBarのコンテンツを構築""" - controls = [] - - # 左側:戻るボタン - if self.show_back: - controls.append( - ft.IconButton( - icon=ft.Icons.ARROW_BACK, - icon_color=ft.Colors.BLUE_GREY_700, - tooltip="戻る", - on_click=self.on_back if self.on_back else None - ) - ) - else: - controls.append(ft.Container(width=48)) # スペーーサー - - # 中央:タイトル - controls.append( - ft.Container( - content=ft.Text( - self.title, - size=18, - weight=ft.FontWeight.W_500, - color=ft.Colors.BLUE_GREY_800 - ), - expand=True, - alignment=ft.alignment.Alignment(0, 0) # 中央揃え - ) - ) - - # 右側:編集ボタン - if self.show_edit: - controls.append( - ft.IconButton( - icon=ft.Icons.EDIT, - icon_color=ft.Colors.BLUE_GREY_700, - tooltip="編集", - on_click=self.on_edit if self.on_edit else None - ) - ) - else: - controls.append(ft.Container(width=48)) # スペーサー - - return ft.Row( - controls, - alignment=ft.MainAxisAlignment.SPACE_BETWEEN, - vertical_alignment=ft.CrossAxisAlignment.CENTER - ) - -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.editing_invoice = None # 編集中の伝票 - self.is_edit_mode = False # 編集モードフラグ - self.is_customer_picker_open = False - self.customer_search_query = "" - self.show_offsets = False - self.chain_verify_result = None - self.is_new_customer_form_open = 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 %H:%M'), - '完了', - invoice.notes - )) - - self.conn.commit() - except Exception as e: - logging.error(f"サンプルデータ作成エラー: {e}") - - def setup_ui(self): - """UIセットアップ""" - # メインコンテンツ - self.main_content = ft.Column([], expand=True) - - # ページ構成 - self.page.add( - ft.Column([ - self.main_content, - ], 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.is_customer_picker_open: - # 顧客選択画面 - self.main_content.controls.append(self.create_customer_picker_screen()) - elif self.current_tab == 0: - # 伝票一覧画面 - self.main_content.controls.append(self._build_invoice_list_screen()) - elif self.current_tab == 1: - # 伝票詳細/編集画面 - self.main_content.controls.append(self._build_invoice_detail_screen()) - elif self.current_tab == 2: - # 新規作成画面 - self.main_content.controls.append(self._build_new_invoice_screen()) - - self.page.update() - - def _build_invoice_list_screen(self) -> ft.Column: - """伝票一覧画面を構築""" - # AppBar(戻るボタンなし、編集ボタンなし) - app_bar = AppBar( - title="伝票一覧", - show_back=False, - show_edit=False - ) - - # 伝票リスト(ズーム対応) - invoice_list = self._build_invoice_list() - zoomable_list = ZoomableContainer( - content=invoice_list, - min_scale=0.8, - max_scale=2.5 - ) - - # 浮動アクションボタン - fab = ft.FloatingActionButton( - icon=ft.Icons.ADD, - on_click=lambda _: self.open_new_invoice(), - tooltip="新規伝票作成" - ) - - return ft.Column([ - app_bar, - ft.Container( - content=zoomable_list, - expand=True, - padding=ft.Padding.all(16) - ), - ], expand=True) - - def _build_invoice_detail_screen(self) -> ft.Column: - """伝票詳細画面を構築""" - if not self.editing_invoice: - return ft.Column([ft.Text("伝票が選択されていません")]) - - # AppBar(戻るボタンあり、編集ボタン条件付き) - is_locked = getattr(self.editing_invoice, 'final_locked', False) - app_bar = AppBar( - title=f"伝票詳細: {self.editing_invoice.invoice_number}", - show_back=True, - show_edit=not is_locked, - on_back=lambda _: self.back_to_list(), - on_edit=lambda _: self.toggle_edit_mode() - ) - - # 伝票詳細コンテンツ(ズーム対応) - detail_content = self._create_edit_existing_screen() - zoomable_content = ZoomableContainer( - content=detail_content, - min_scale=0.8, - max_scale=2.5 - ) - - return ft.Column([ - app_bar, - ft.Container( - content=zoomable_content, - expand=True, - padding=ft.Padding.all(16) - ), - ], expand=True) - - def _build_new_invoice_screen(self) -> ft.Column: - """新規作成画面を構築""" - # AppBar(戻るボタンあり、編集ボタンなし) - app_bar = AppBar( - title="新規伝票作成", - show_back=True, - show_edit=False, - on_back=lambda _: self.back_to_list() - ) - - # 新規作成コンテンツ(ズーム対応) - new_content = self.create_slip_input_screen() - zoomable_content = ZoomableContainer( - content=new_content, - min_scale=0.8, - max_scale=2.5 - ) - - return ft.Column([ - app_bar, - ft.Container( - content=zoomable_content, - expand=True, - padding=ft.Padding.all(16) - ), - ], expand=True) - - def back_to_list(self): - """一覧画面に戻る""" - self.current_tab = 0 - self.editing_invoice = None - self.update_main_content() - - def toggle_edit_mode(self): - """編集モードを切り替え""" - if hasattr(self, 'is_detail_edit_mode'): - self.is_detail_edit_mode = not self.is_detail_edit_mode - else: - self.is_detail_edit_mode = True - self.update_main_content() - - def _build_invoice_list(self) -> ft.Column: - """伝票リストを構築""" - # 履歴データ読み込み - slips = self.load_slips() - logging.info(f"伝票データ取得: {len(slips)}件") - - if not self.show_offsets: - slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))] - logging.info(f"赤伝除外後: {len(slips)}件") - - def on_toggle_offsets(e): - self.show_offsets = bool(e.control.value) - self.update_main_content() - - def on_verify_chain(_): - """チェーン検証""" - try: - result = self.app_service.invoice.verify_chain() - self.chain_verify_result = result - self.update_main_content() - except Exception as e: - logging.error(f"チェーン検証エラー: {e}") - - # 履歴カードリスト - slip_cards = [] - for i, slip in enumerate(slips): - logging.info(f"伝票{i}: {type(slip)}") - card = self.create_slip_card(slip) - slip_cards.append(card) - - logging.info(f"カード作成数: {len(slip_cards)}") - - return ft.Column([ - # 検証結果表示(あれば) - ft.Container( - content=self._build_chain_verify_result(), - margin=ft.Margin.only(bottom=10), - ) if self.chain_verify_result else ft.Container(height=0), - - # 伝票リスト - ft.Column( - controls=slip_cards, - spacing=10, - scroll=ft.ScrollMode.AUTO, - expand=True, - ), - ]) - 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() - - # AppBar(戻るボタンあり) - app_bar = AppBar( - title="顧客選択", - show_back=True, - show_edit=False, - on_back=back - ) - - 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 - # 新規伝票作成中の場合は顧客を設定 - if hasattr(self, 'editing_invoice') and self.editing_invoice: - self.editing_invoice.customer = customer - logging.info(f"伝票に顧客を設定: {customer.formal_name}") - else: - 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), - ft.Container(expand=True), - ft.IconButton( - ft.Icons.PERSON_ADD, - tooltip="新規顧客追加", - icon_color=ft.Colors.WHITE, - on_click=lambda _: self.open_new_customer_form(), - ), - ] - ), - padding=ft.padding.all(15), - bgcolor=ft.Colors.BLUE_GREY, - ) - - content = ft.Column( - [ - app_bar, # AppBarを追加 - 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 save_as_draft(self, _): - """下書きとして保存""" - try: - # 下書き伝票を作成 - draft_invoice = self.app_service.invoice.create_draft_invoice( - customer=self.selected_customer, - document_type=DocumentType.DRAFT, - amount=int(self.amount_value or "0"), - notes="下書き" - ) - - if draft_invoice: - logging.info(f"下書き保存成功: {draft_invoice.invoice_number}") - # 一覧を更新 - self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.back_to_list() - else: - logging.error("下書き保存失敗") - except Exception as e: - logging.error(f"下書き保存エラー: {e}") - - 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 doc_type == self.selected_document_type else ft.Colors.GREY_300, - color=ft.Colors.WHITE if doc_type == self.selected_document_type 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) - - # 下書きボタン(新規作成時のみ表示) - draft_button = ft.Container( - content=ft.ElevatedButton( - content=ft.Row([ - ft.Icon(ft.Icons.DRAFTS, size=16), - ft.Text("下書きとして保存", size=12), - ], spacing=5), - style=ft.ButtonStyle( - bgcolor=ft.Colors.ORANGE_500, - color=ft.Colors.WHITE, - ), - on_click=self.save_as_draft, - ), - margin=ft.margin.only(top=10), - ) - - 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, - draft_button, # 下書きボタンを追加 - ]), - 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.Container(width=8), # スペースを追加 - ft.IconButton( - ft.Icons.PERSON_SEARCH, - tooltip="顧客を選択", - on_click=self.open_customer_picker, - ), - ], - alignment=ft.MainAxisAlignment.SPACE_BETWEEN, # 検索ボタンを右端に配置 - ), - 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() - - def on_verify_chain(e=None): - res = self.app_service.invoice.invoice_repo.verify_chain() - self.chain_verify_result = res - 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=16, weight=ft.FontWeight.BOLD), # 文字を小さく - ft.Container(expand=True), - ft.Row([ - ft.IconButton( - ft.Icons.VERIFIED, - tooltip="チェーン検証", - icon_color=ft.Colors.BLUE_300, - icon_size=16, # アイコンを小さく - on_click=on_verify_chain, - ), - ft.Row( - [ - ft.Text("赤伝", size=10, color=ft.Colors.WHITE), # 文字を小さく - ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets), - ], - spacing=3, # 間隔を狭める - ), - ft.IconButton( - ft.Icons.CLEAR_ALL, - icon_size=16, # アイコンを小さく - ), - ], spacing=3), # 間隔を狭める - ]), - padding=ft.padding.all(8), # パディングを狭める - bgcolor=ft.Colors.BLUE_GREY, - ), - - # 検証結果表示(あれば) - ft.Container( - content=self._build_chain_verify_result(), - margin=ft.Margin.only(bottom=5), # 間隔を狭める - ) if self.chain_verify_result else ft.Container(height=0), - - # 履歴リスト(極限密度表示) - ft.Column( - controls=slip_cards, - spacing=0, # カード間隔を0pxに - scroll=ft.ScrollMode.AUTO, - expand=True, - ), - ]), - expand=True, - ) - - def can_create_offset_invoice(self, invoice: Invoice) -> bool: - """赤伝発行可能かチェック""" - # LOCK済み伝票であること - if not getattr(invoice, 'final_locked', False): - return False - - # すでに赤伝が存在しないこと - try: - import sqlite3 - with sqlite3.connect('sales.db') as conn: - cursor = conn.cursor() - cursor.execute( - 'SELECT COUNT(*) FROM invoices WHERE offset_target_uuid = ?', - (invoice.uuid,) - ) - offset_count = cursor.fetchone()[0] - return offset_count == 0 - except Exception as e: - logging.error(f"赤伝存在チェックエラー: {e}") - return False - - 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 %H:%M") - status = "赤伝" if getattr(slip, "is_offset", False) else "完了" - # 最初の商品名を取得(複数ある場合は「他」を付与) - if slip.items and len(slip.items) > 0: - first_item_name = slip.items[0].description - if len(slip.items) > 1: - first_item_name += "(他" + str(len(slip.items) - 1) + ")" - else: - first_item_name = "" - else: - slip_id, slip_type, customer_name, amount, date, status, description, created_at = slip - date = date.strftime("%Y-%m-%d %H:%M") - first_item_name = description or "" - - # タイプに応じたアイコンと色 - 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 on_single_tap(_): - """シングルタップ:詳細表示""" - if isinstance(slip, Invoice): - self.open_invoice_detail(slip) - - def on_double_tap(_): - """ダブルタップ:編集モード切替""" - if isinstance(slip, Invoice): - self.open_invoice_edit(slip) - - def on_long_press(_): - """長押し:コンテキストメニュー""" - self.show_context_menu(slip) - - # 赤伝ボタンの表示条件チェック - show_offset_button = False - if isinstance(slip, Invoice): - show_offset_button = self.can_create_offset_invoice(slip) - - # 長押しメニューで操作するため、ボタンは不要 - - display_amount = amount - if isinstance(slip, Invoice) and getattr(slip, "is_offset", False): - display_amount = -abs(amount) - - return ft.GestureDetector( - content=ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Row([ - ft.Container( - content=ft.Text(config["icon"], size=16), # アイコンを少し大きく - width=28, - height=28, - bgcolor=config["color"], - border_radius=14, - alignment=ft.alignment.Alignment(0, 0), - ), - ft.Container( - content=ft.Column([ - ft.Text(slip_type, size=7, weight=ft.FontWeight.BOLD), # タイプ文字をさらに小さく - ft.Text(customer_name, size=15, weight=ft.FontWeight.W_500), # 顧客名を1.5倍に - ft.Row([ - ft.Text(first_item_name, size=12, color=ft.Colors.GREY_600), # 商品名をさらに大きく - ft.Container(expand=True), # スペースを取る - ft.Text(f"¥{display_amount:,.0f}", size=11, weight=ft.FontWeight.BOLD), # 金額を右寄せ - ]), - ], - spacing=0, # 行間を最小化 - tight=True, # 余白を最小化 - ), - expand=True, - ), - ]), - ft.Container(height=0), # 間隔を完全に削除 - ft.Row([ - ft.Text(f"{date} | {status}", size=9, color=ft.Colors.GREY_600), # 日付を大きく - ft.Container(expand=True), # スペースを取る - # 赤伝ボタン(条件付きで表示) - ft.Container( - content=ft.IconButton( - icon=ft.icons.REMOVE_CIRCLE_OUTLINE, - icon_color=ft.Colors.RED_500, - icon_size=16, - tooltip="赤伝発行", - on_click=lambda _: self.create_offset_invoice_dialog(slip), - disabled=not show_offset_button - ) if show_offset_button else ft.Container(width=20), - ), - ]), - ], - spacing=0, # カラムの行間を最小化 - tight=True, # 余白を最小化 - ), - padding=ft.padding.all(2), # パディングを最小化 - ), - elevation=0, - ), - on_tap=on_single_tap, - on_double_tap=on_double_tap, - on_long_press=on_long_press, - ) - - def create_offset_invoice_dialog(self, invoice: Invoice): - """赤伝発行確認ダイアログ""" - def close_dialog(_): - self.dialog.open = False - self.update_main_content() - - def confirm_create_offset(_): - # 赤伝を発行 - offset_invoice = self.app_service.invoice.create_offset_invoice( - invoice.uuid, - f"相殺伝票: {invoice.invoice_number}" - ) - if offset_invoice: - logging.info(f"赤伝発行成功: {offset_invoice.invoice_number}") - # 一覧を更新 - self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.update_main_content() - else: - logging.error(f"赤伝発行失敗: {invoice.invoice_number}") - close_dialog(_) - - # 確認ダイアログ - self.dialog = ft.AlertDialog( - modal=True, - title=ft.Text("赤伝発行確認"), - content=ft.Column([ - ft.Text(f"以下の伝票の赤伝を発行します。"), - ft.Container(height=10), - ft.Text(f"伝票番号: {invoice.invoice_number}"), - ft.Text(f"顧客: {invoice.customer.formal_name}"), - ft.Text(f"金額: ¥{invoice.total_amount:,.0f}"), - ft.Container(height=10), - ft.Text("赤伝発行後は取り消せません。よろしいですか?", - color=ft.Colors.RED, weight=ft.FontWeight.BOLD), - ], tight=True), - actions=[ - ft.TextButton("キャンセル", on_click=close_dialog), - ft.ElevatedButton( - "赤伝発行", - on_click=confirm_create_offset, - style=ft.ButtonStyle( - color=ft.Colors.WHITE, - bgcolor=ft.Colors.RED_500 - ) - ), - ], - actions_alignment=ft.MainAxisAlignment.END, - ) - - self.dialog.open = True - self.update_main_content() - - def show_context_menu(self, slip): - """コンテキストメニューを表示""" - if not isinstance(slip, Invoice): - return - - def close_dialog(_): - self.dialog.open = False - self.update_main_content() - - def edit_invoice(_): - self.open_invoice_edit(slip) - close_dialog(_) - - def delete_invoice(_): - self.delete_invoice(slip.uuid) - close_dialog(_) - - def create_offset(_): - self.create_offset_invoice_dialog(slip) - close_dialog(_) - - # メニューアイテムの構築 - menu_items = [] - - # 編集メニュー - if not getattr(slip, 'final_locked', False): - menu_items.append( - ft.PopupMenuItem( - text=ft.Row([ - ft.Icon(ft.Icons.EDIT, size=16), - ft.Text("編集", size=14), - ], spacing=8), - on_click=edit_invoice - ) - ) - - # 赤伝発行メニュー - if self.can_create_offset_invoice(slip): - menu_items.append( - ft.PopupMenuItem( - text=ft.Row([ - ft.Icon(ft.Icons.REMOVE_CIRCLE, size=16), - ft.Text("赤伝発行", size=14), - ], spacing=8), - on_click=create_offset - ) - ) - - # 削除メニュー - menu_items.append( - ft.PopupMenuItem( - text=ft.Row([ - ft.Icon(ft.Icons.DELETE, size=16), - ft.Text("削除", size=14), - ], spacing=8), - on_click=delete_invoice - ) - ) - - # コンテキストメニューダイアログ - self.dialog = ft.AlertDialog( - modal=True, - title=ft.Text(f"操作選択: {slip.invoice_number}"), - content=ft.Column(menu_items, tight=True, spacing=2), - actions=[ - ft.TextButton("キャンセル", on_click=close_dialog), - ], - actions_alignment=ft.MainAxisAlignment.END, - ) - - self.dialog.open = True - self.update_main_content() - - def open_invoice_detail(self, invoice: Invoice): - """伝票詳細を開く""" - self.editing_invoice = invoice - self.current_tab = 1 - self.is_detail_edit_mode = False # 表示モードで開く - self.update_main_content() - - def open_invoice_edit(self, invoice: Invoice): - """伝票編集を開く""" - self.editing_invoice = invoice - self.current_tab = 1 - self.is_detail_edit_mode = True # 編集モードで開く - self.update_main_content() - - def delete_invoice(self, invoice_uuid: str): - """伝票を削除""" - try: - success = self.app_service.invoice.delete_invoice_by_uuid(invoice_uuid) - if success: - logging.info(f"伝票削除成功: {invoice_uuid}") - # リストを更新 - self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.update_main_content() - else: - logging.error(f"伝票削除失敗: {invoice_uuid}") - except Exception as e: - logging.error(f"伝票削除エラー: {e}") - - def _build_chain_verify_result(self) -> ft.Control: - if not self.chain_verify_result: - return ft.Container(height=0) - r = self.chain_verify_result - ok = r.get("ok", False) - checked = r.get("checked", 0) - errors = r.get("errors", []) - if ok: - return ft.Container( - content=ft.Row([ - ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN, size=20), - ft.Text(f"チェーン検証 OK ({checked}件)", size=14, color=ft.Colors.GREEN), - ]), - bgcolor=ft.Colors.GREEN_50, - padding=ft.Padding.all(10), - border_radius=8, - ) - else: - return ft.Container( - content=ft.Column([ - ft.Row([ - ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED, size=20), - ft.Text(f"チェーン検証 NG (checked={checked})", size=14, color=ft.Colors.RED), - ]), - ft.Text(f"エラー: {errors}", size=12, color=ft.Colors.RED_700), - ]), - bgcolor=ft.Colors.RED_50, - padding=ft.Padding.all(10), - border_radius=8, - ) - - def open_invoice_edit(self, invoice: Invoice): - """伝票編集画面を開く""" - self.editing_invoice = invoice - self.is_edit_mode = True - self.selected_customer = invoice.customer - self.selected_document_type = invoice.document_type - self.amount_value = str(invoice.items[0].unit_price if invoice.items else "0") - self.is_detail_edit_mode = False # 初期はビューモード - self.current_tab = 1 # 詳細編集タブに切り替え - self.update_main_content() - - def open_new_customer_form(self): - """新規顧客フォームを開く(画面内遷移)""" - self.is_new_customer_form_open = True - self.update_main_content() - - def create_invoice_edit_screen(self) -> ft.Container: - """伝票編集画面(新規・編集統合)""" - # 常に詳細編集画面を使用 - if not self.editing_invoice: - # 新規伝票の場合は空のInvoiceを作成 - from models.invoice_models import Invoice, Customer, DocumentType - default_customer = Customer( - id=0, - name="選択してください", - formal_name="選択してください", - address="", - phone="" - ) - self.editing_invoice = Invoice( - customer=default_customer, - date=datetime.now(), - items=[], - document_type=DocumentType.SALES, - invoice_number="NEW-" + str(int(datetime.now().timestamp())) # 新規伝票番号 - ) - self.is_detail_edit_mode = True # 新規作成モード - - # 既存・新規共通で詳細編集画面を返す - return self._create_edit_existing_screen() - - def _create_edit_existing_screen(self) -> ft.Container: - """既存伝票の編集画面(新規・編集共通)""" - # 編集不可チェック(新規作成時はFalse) - is_new_invoice = self.editing_invoice.invoice_number.startswith("NEW-") - - # LOCK条件:明示的に確定された場合のみLOCK - # PDF生成だけではLOCKしない(お試しPDFを許可) - pdf_generated = getattr(self.editing_invoice, 'pdf_generated_at', None) is not None - chain_hash = getattr(self.editing_invoice, 'chain_hash', None) is not None - final_locked = getattr(self.editing_invoice, 'final_locked', False) # 明示的確定フラグ - - is_locked = final_locked if not is_new_invoice else False - is_view_mode = not getattr(self, 'is_detail_edit_mode', False) - - # デバッグ用にLOCK状態をログ出力 - logging.info(f"伝票LOCK状態: {self.editing_invoice.invoice_number}") - logging.info(f" PDF生成: {pdf_generated}") - logging.info(f" チェーン収容: {chain_hash}") - logging.info(f" 明示的確定: {final_locked}") - logging.info(f" LOCK状態: {is_locked}") - logging.info(f" 新規伝票: {is_new_invoice}") - logging.info(f" 編集モード: {getattr(self, 'is_detail_edit_mode', False)}") - logging.info(f" 表示モード: {is_view_mode}") - - # 伝票種類選択(ヘッダーに移動) - document_types = list(DocumentType) - - # 明細テーブル - if is_view_mode: - items_table = self._create_view_mode_table(self.editing_invoice.items) - else: - items_table = self._create_edit_mode_table(self.editing_invoice.items, is_locked) - - # 顧客表示・選択 - def select_customer(): - """顧客選択画面を開く""" - self.current_tab = 2 # 顧客選択タブ - self.update_main_content() - - # 編集モード時は顧客名入力フィールドを表示 - if not is_view_mode and not is_locked: - # 編集モード:顧客名入力フィールド - customer_field = ft.TextField( - label="顧客名", - value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "", - disabled=is_locked, - width=300, - ) - - def update_customer_name(e): - """顧客名を更新""" - if self.editing_invoice: - # 既存顧客を検索、なければ新規作成 - customer_name = e.control.value or "" - found_customer = None - for customer in self.app_service.customer.get_all_customers(): - if customer.name == customer_name or customer.formal_name == customer_name: - found_customer = customer - break - - if found_customer: - self.editing_invoice.customer = found_customer - else: - # 新規顧客を作成 - from models.invoice_models import Customer - self.editing_invoice.customer = Customer( - id=0, - name=customer_name, - formal_name=customer_name, - address="", - phone="" - ) - - customer_field.on_change = update_customer_name - - customer_display = ft.Container( - content=ft.Row([ - customer_field, # 顧客ラベルを削除 - ft.ElevatedButton( - "選択", - icon=ft.Icons.PERSON_SEARCH, - on_click=lambda _: select_customer(), - style=ft.ButtonStyle( - bgcolor=ft.Colors.BLUE_600, - color=ft.Colors.WHITE - ) - ), - ]), - padding=ft.padding.symmetric(horizontal=10, vertical=5), - bgcolor=ft.Colors.BLUE_50, - border_radius=5, - ) - elif is_new_invoice: - # 新規作成時も同じ表示 - customer_field = ft.TextField( - label="顧客名", - value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "", - disabled=is_locked, - width=300, - ) - - def update_customer_name(e): - """顧客名を更新""" - if self.editing_invoice: - # 既存顧客を検索、なければ新規作成 - customer_name = e.control.value or "" - found_customer = None - for customer in self.app_service.customer.get_all_customers(): - if customer.name == customer_name or customer.formal_name == customer_name: - found_customer = customer - break - - if found_customer: - self.editing_invoice.customer = found_customer - else: - # 新規顧客を作成 - from models.invoice_models import Customer - self.editing_invoice.customer = Customer( - id=0, - name=customer_name, - formal_name=customer_name, - address="", - phone="" - ) - - customer_field.on_change = update_customer_name - - customer_display = ft.Container( - content=ft.Row([ - customer_field, # 顧客ラベルを削除 - ft.ElevatedButton( - "選択", - icon=ft.Icons.PERSON_SEARCH, - on_click=lambda _: select_customer(), - style=ft.ButtonStyle( - bgcolor=ft.Colors.BLUE_600, - color=ft.Colors.WHITE - ) - ), - ]), - padding=ft.padding.symmetric(horizontal=10, vertical=5), - bgcolor=ft.Colors.BLUE_50, - border_radius=5, - ) - else: - # 既存伝票は表示のみ - customer_display = ft.Container( - content=ft.Row([ - ft.Text(self.editing_invoice.customer.name), # 顧客ラベルを削除 - ]), - padding=ft.padding.symmetric(horizontal=10, vertical=5), - bgcolor=ft.Colors.GREY_100, - border_radius=5, - ) - - # 備考フィールド - notes_field = ft.TextField( - label="備考", - value=getattr(self.editing_invoice, 'notes', ''), - disabled=is_locked or is_view_mode, - multiline=True, - min_lines=2, - max_lines=3, - ) - - def toggle_edit_mode(_): - """編集モード切替""" - old_mode = getattr(self, 'is_detail_edit_mode', False) - self.is_detail_edit_mode = not old_mode - logging.debug(f"Toggle edit mode: {old_mode} -> {self.is_detail_edit_mode}") - self.update_main_content() - - def save_changes(_): - if is_locked: - return - - # 伝票を更新(現在の明細を保持) - # テーブルから実際の値を取得して更新 - self.editing_invoice.notes = notes_field.value - self.editing_invoice.document_type = self.selected_document_type - - # TODO: テーブルの明細データを取得して更新 - # 現在は編集された明細データが反映されていない - logging.info(f"更新前明細件数: {len(self.editing_invoice.items)}") - for i, item in enumerate(self.editing_invoice.items): - logging.info(f" 明細{i+1}: {item.description} x{item.quantity} @¥{item.unit_price}") - - # DBに保存(新規・更新共通) - try: - if is_new_invoice: - # 新規作成 - logging.info(f"=== 新規伝票作成開 ===") - logging.info(f"顧客情報: {self.editing_invoice.customer.name} (ID: {self.editing_invoice.customer.id})") - logging.info(f"伝票種類: {self.editing_invoice.document_type.value}") - logging.info(f"明細件数: {len(self.editing_invoice.items)}") - - # 顧客を先にDBに保存(新規顧客の場合) - if self.editing_invoice.customer.id == 0: - logging.info(f"新規顧客をDBに保存します: {self.editing_invoice.customer.name}") - # 新規顧客をDBに保存 - customer_id = self.app_service.customer.create_customer( - name=self.editing_invoice.customer.name, - formal_name=self.editing_invoice.customer.formal_name, - address=self.editing_invoice.customer.address, - phone=self.editing_invoice.customer.phone - ) - logging.info(f"create_customer戻り値: {customer_id}") - if customer_id > 0: # IDが正しく取得できたかチェック - self.editing_invoice.customer.id = customer_id - logging.info(f"新規顧客をDBに保存: {self.editing_invoice.customer.name} (ID: {customer_id})") - else: - logging.error(f"顧客保存失敗: {self.editing_invoice.customer.name}") - return - - # 合計金額は表示時に計算するため、DBには保存しない - amount = 0 # ダミー値(実際は表示時に計算) - - logging.info(f"伝票作成パラメータ: customer.id={self.editing_invoice.customer.id}, document_type={self.editing_invoice.document_type}, amount={amount}") - - success = self.app_service.invoice.create_invoice( - customer=self.editing_invoice.customer, - document_type=self.editing_invoice.document_type, - amount=amount, - notes=getattr(self.editing_invoice, 'notes', ''), - items=self.editing_invoice.items # UIの明細を渡す - ) - logging.info(f"create_invoice戻り値: {success}") - if success: - logging.info(f"伝票作成成功: {self.editing_invoice.invoice_number}") - # 一覧を更新して新規作成画面を閉じる - self.invoices = self.app_service.invoice.get_recent_invoices(20) - logging.info(f"更新後伝票件数: {len(self.invoices)}") - self.editing_invoice = None - self.current_tab = 0 # 一覧タブに戻る - self.update_main_content() - else: - logging.error(f"伝票作成失敗: {self.editing_invoice.invoice_number}") - else: - # 更新 - logging.info(f"=== 伝票更新開 ===") - success = self.app_service.invoice.update_invoice(self.editing_invoice) - if success: - logging.info(f"伝票更新成功: {self.editing_invoice.invoice_number}") - # 一覧を更新して編集画面を閉じる - self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.editing_invoice = None - self.current_tab = 0 # 一覧タブに戻る - self.update_main_content() - else: - logging.error(f"伝票更新失敗: {self.editing_invoice.invoice_number}") - except Exception as e: - logging.error(f"伝票保存エラー: {e}") - import traceback - logging.error(f"詳細エラー: {traceback.format_exc()}") - - # 編集モード終了(ビューモードに戻る) - self.is_detail_edit_mode = False # ビューモードに戻る - self.update_main_content() - - def cancel_edit(_): - self.is_detail_edit_mode = False - self.is_edit_mode = False - self.editing_invoice = None - self.current_tab = 0 # 一覧タブに戻る - self.update_main_content() - - return ft.Container( - content=ft.Column([ - # ヘッダー - ft.Container( - content=ft.Row([ - ft.Container(expand=True), - # コンパクトな伝票種類選択(セグメント化) - ft.Container( - content=ft.Row([ - ft.GestureDetector( - content=ft.Container( - content=ft.Text( - doc_type.value, - size=10, - color=ft.Colors.WHITE if doc_type == self.editing_invoice.document_type else ft.Colors.GREY_600, - weight=ft.FontWeight.BOLD if doc_type == self.editing_invoice.document_type else ft.FontWeight.NORMAL, - ), - padding=ft.padding.symmetric(horizontal=8, vertical=4), - bgcolor=ft.Colors.BLUE_600 if doc_type == self.editing_invoice.document_type else ft.Colors.GREY_300, - border_radius=ft.border_radius.all(4), - margin=ft.margin.only(right=1), - ), - on_tap=lambda _, dt=doc_type: self.select_document_type(dt.value) if not is_locked and not is_view_mode else None, - ) for doc_type in document_types - ]), - padding=ft.padding.all(2), - bgcolor=ft.Colors.GREY_200, - border_radius=ft.border_radius.all(6), - margin=ft.margin.only(right=10), - ), - ft.ElevatedButton( - content=ft.Text("編集" if is_view_mode else "保存"), - style=ft.ButtonStyle( - bgcolor=ft.Colors.BLUE_600 if is_view_mode else ft.Colors.GREEN_600, - ), - on_click=toggle_edit_mode if is_view_mode else save_changes, - disabled=is_locked, - width=70, - height=30, - ) if not is_locked else ft.Container(), - ft.Container(width=5), - ft.IconButton(ft.Icons.CLOSE, on_click=cancel_edit), - ]), - padding=ft.padding.symmetric(horizontal=15, vertical=8), - bgcolor=ft.Colors.BLUE_GREY, - ), - - # 基本情報行(コンパクトに) - ft.Container( - content=ft.Row([ - ft.Text(f"{self.editing_invoice.invoice_number} | {self.editing_invoice.date.strftime('%Y/%m/%d %H:%M')} | {self.editing_invoice.customer.name}", size=12, weight=ft.FontWeight.BOLD), - ft.Container(expand=True), - ]), - padding=ft.padding.symmetric(horizontal=15, vertical=3), - bgcolor=ft.Colors.GREY_50, - ), - - # 顧客名入力(編集モードまたは新規作成時) - customer_display if (not is_view_mode and not is_locked) or is_new_invoice else ft.Container(height=0), - - # 明細テーブル(フレキシブルに) - ft.Container( - content=ft.Column([ - ft.Container( - content=items_table, - height=400, # 高さを拡大して見やすく - border=ft.border.all(1, ft.Colors.GREY_300), - border_radius=5, - padding=ft.padding.all(1), # パディングを最小化 - width=None, # 幅を可変に - expand=True, # 利用可能な幅を全て使用 - ), - ft.Container(height=10), - # 合計金額表示 - ft.Container( - content=ft.Row([ - ft.Text("合計: ", size=14, weight=ft.FontWeight.BOLD), # 左に詰める - ft.Text( - f"¥{sum(item.subtotal for item in self.editing_invoice.items):,}", - size=16, - weight=ft.FontWeight.BOLD, - color=ft.Colors.BLUE_600 - ), - ft.Container(expand=True), # スペースを取る - # +ボタンを右端に配置 - ft.IconButton( - ft.Icons.ADD_CIRCLE_OUTLINE, - tooltip="行を追加", - icon_color=ft.Colors.GREEN_600, - disabled=is_locked or is_view_mode, - on_click=lambda _: self._add_item_row(), - ) if not is_locked and not is_view_mode else ft.Container(), - ]), - padding=ft.padding.symmetric(horizontal=5, vertical=8), # 左右のパディングを減らす - bgcolor=ft.Colors.GREY_100, - border_radius=5, - ), - ]), - padding=ft.padding.all(15), - expand=True, # 明細部分が最大限のスペースを占有 - ), - - # 備考(コンパクト) - ft.Container( - content=ft.Column([ - ft.Text("備考", size=12, weight=ft.FontWeight.BOLD), - ft.Container(height=3), - notes_field, - ft.Container(height=5), - ft.Text("🔒 税務署提出済みは編集できません" if is_locked else "✅ " + ("編集モード" if not is_view_mode else "ビューモード"), - size=11, color=ft.Colors.RED_600 if is_locked else (ft.Colors.GREEN_600 if not is_view_mode else ft.Colors.BLUE_600)), - ft.Container(height=10), - # PDF生成ボタンを追加 - ft.ElevatedButton( - content=ft.Row([ - ft.Icon(ft.Icons.DOWNLOAD, size=16), - ft.Container(width=5), - ft.Text("PDF生成", size=12, color=ft.Colors.WHITE), - ]), - style=ft.ButtonStyle( - bgcolor=ft.Colors.BLUE_600, - padding=ft.padding.symmetric(horizontal=15, vertical=10), - ), - on_click=lambda _: self.generate_pdf_from_edit(), - width=120, - height=40, - ) if not is_locked else ft.Container(), - ]), - padding=ft.padding.symmetric(horizontal=15, vertical=10), - bgcolor=ft.Colors.GREY_50, - ), - ]), - expand=True, - ) - def generate_pdf_from_edit(self): - """編集画面からPDFを生成""" - if not self.editing_invoice: - return - - try: - pdf_path = self.app_service.invoice.regenerate_pdf(self.editing_invoice.uuid) - if pdf_path: - self.editing_invoice.file_path = pdf_path - self.editing_invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() - logging.info(f"PDF生成完了: {pdf_path}") - # TODO: 成功メッセージ表示 - else: - logging.error("PDF生成失敗") - # TODO: エラーメッセージ表示 - except Exception as e: - logging.error(f"PDF生成エラー: {e}") - # TODO: エラーメッセージ表示 - - def _add_item_row(self): - """明細行を追加""" - if not self.editing_invoice: - return - - # 空の明細行を追加(デフォルト値なし) - new_item = InvoiceItem( - description="", - quantity=0, - unit_price=0, - ) - - # 元のinvoice.itemsに直接追加 - self.editing_invoice.items.append(new_item) - - # UIを更新 - self.update_main_content() - - def _delete_item_row(self, index: int): - """明細行を削除""" - if not self.editing_invoice or index >= len(self.editing_invoice.items): - return - - # 行を削除(最低1行は残す) - if len(self.editing_invoice.items) > 1: - del self.editing_invoice.items[index] - self.update_main_content() - - def _update_item_field(self, item_index: int, field_name: str, value: str): - """明細フィールドを更新""" - if not self.editing_invoice or item_index >= len(self.editing_invoice.items): - return - - item = self.editing_invoice.items[item_index] - - # デバッグ用:更新前の値をログ出力 - old_value = getattr(item, field_name) - logging.debug(f"Updating item {item_index} {field_name}: '{old_value}' -> '{value}'") - - if field_name == 'description': - item.description = value - elif field_name == 'quantity': - try: - # 空文字の場合は1を設定 - if not value or value.strip() == '': - item.quantity = 1 - else: - item.quantity = int(value) - logging.debug(f"Quantity updated to: {item.quantity}") - except ValueError as e: - item.quantity = 1 - logging.error(f"Quantity update error: {e}") - elif field_name == 'unit_price': - try: - # 空文字の場合は0を設定 - if not value or value.strip() == '': - item.unit_price = 0 - else: - item.unit_price = int(value) - logging.debug(f"Unit price updated to: {item.unit_price}") - except ValueError as e: - item.unit_price = 0 - logging.error(f"Unit price update error: {e}") - - # 合計金額を更新するために画面を更新 - # 空行削除ロジック:商品名が無く数量も単価も0なら削除 - self._remove_empty_items() - self.update_main_content() - - def _remove_empty_items(self): - """商品名が無く数量も単価も0の明細を削除""" - if not self.editing_invoice: - return - - # 空行を特定(ただし最低1行は残す) - non_empty_items = [] - empty_count = 0 - - for item in self.editing_invoice.items: - if (not item.description or item.description.strip() == "") and \ - item.quantity == 0 and \ - item.unit_price == 0: - empty_count += 1 - # 最低1行は残すため、空行が複数ある場合のみ削除 - if empty_count > 1: - continue # 削除 - non_empty_items.append(item) - - self.editing_invoice.items = non_empty_items - - def _create_view_mode_table(self, items: List[InvoiceItem]) -> ft.Column: - """表示モード:フレキシブルな表形式で整然と表示""" - # ヘッダー行 - header_row = ft.Row([ - ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), # 可変幅 - ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅 - ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Container(width=35), # 削除ボタン用スペースを確保 - ]) - - # データ行 - data_rows = [] - for i, item in enumerate(items): - row = ft.Row([ - ft.Text(item.description, size=12, expand=True), # 可変幅 - ft.Text(str(item.quantity), size=12, width=35, text_align=ft.TextAlign.RIGHT), # 固定幅 - ft.Text(f"¥{item.unit_price:,}", size=12, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 - ft.Text(f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 - ft.Container(width=35), # 削除ボタン用スペース - ]) - data_rows.append(row) - - return ft.Column([ - header_row, - ft.Divider(height=1, color=ft.Colors.GREY_400), - ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高さを制限 - ]) - - def _create_edit_mode_table(self, items: List[InvoiceItem], is_locked: bool) -> ft.Column: - """編集モード:フレキシブルな表形式""" - # 自動空行追加を無効化(ユーザーが明示的に追加する場合のみ) - # TODO: 必要に応じて空行追加ボタンを提供 - - # 空行の自動追加を無効化 - pass - - # ヘッダー行 - header_row = ft.Row([ - ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), # 可変幅 - ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅 - ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Container(width=35), # 削除ボタン用スペースを確保 - ]) - - # データ行 - data_rows = [] - for i, item in enumerate(items): - # 商品名フィールド - product_field = ft.TextField( - value=item.description, - text_size=12, - height=28, - width=None, # 幅を可変に - expand=True, # 可変幅 - border=ft.border.all(1, ft.Colors.BLUE_200), - bgcolor=ft.Colors.WHITE, - content_padding=ft.padding.all(5), # 内部余白を最小化 - on_change=lambda e, idx=i: self._update_item_field(idx, 'description', e.control.value), - ) - - # 数量フィールド - quantity_field = ft.TextField( - value=str(item.quantity), - text_size=12, - height=28, - width=35, # 固定幅 - text_align=ft.TextAlign.RIGHT, - border=ft.border.all(1, ft.Colors.BLUE_200), - bgcolor=ft.Colors.WHITE, - content_padding=ft.padding.all(5), # 内部余白を最小化 - on_change=lambda e, idx=i: self._update_item_field(idx, 'quantity', e.control.value), - keyboard_type=ft.KeyboardType.NUMBER, - ) - - # 単価フィールド - unit_price_field = ft.TextField( - value=f"{item.unit_price:,}", - text_size=12, - height=28, - width=70, # 固定幅 - text_align=ft.TextAlign.RIGHT, - border=ft.border.all(1, ft.Colors.BLUE_200), - bgcolor=ft.Colors.WHITE, - content_padding=ft.padding.all(5), # 内部余白を最小化 - on_change=lambda e, idx=i: self._update_item_field(idx, 'unit_price', e.control.value.replace(',', '')), - keyboard_type=ft.KeyboardType.NUMBER, - ) - - # 削除ボタン - delete_button = ft.IconButton( - ft.Icons.DELETE_OUTLINE, - tooltip="行を削除", - icon_color=ft.Colors.RED_600, - disabled=is_locked, - icon_size=16, - on_click=lambda _, idx=i: self._delete_item_row(idx), - ) - - # データ行 - row = ft.Row([ - product_field, - quantity_field, - unit_price_field, - ft.Text(f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 - delete_button, - ]) - data_rows.append(row) - - return ft.Column([ - header_row, - ft.Divider(height=1, color=ft.Colors.GREY_400), - ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高さを制限 - ]) - def create_new_customer_screen(self) -> ft.Container: - """新規顧客登録画面""" - name_field = ft.TextField(label="顧客名(略称)") - formal_name_field = ft.TextField(label="正式名称") - address_field = ft.TextField(label="住所") - phone_field = ft.TextField(label="電話番号") - - def save_customer(_): - name = (name_field.value or "").strip() - formal_name = (formal_name_field.value or "").strip() - address = (address_field.value or "").strip() - phone = (phone_field.value or "").strip() - if not name or not formal_name: - # TODO: エラー表示 - return - new_customer = self.app_service.customer.create_customer(name, formal_name, address, phone) - if new_customer: - self.customers = self.app_service.customer.get_all_customers() - self.selected_customer = new_customer - logging.info(f"新規顧客登録: {new_customer.formal_name}") - self.is_customer_picker_open = False - self.is_new_customer_form_open = False - self.update_main_content() - else: - logging.error("新規顧客登録失敗") - - def cancel(_): - self.is_new_customer_form_open = False - self.update_main_content() - - return ft.Container( - content=ft.Column([ - ft.Container( - content=ft.Row([ - ft.IconButton(ft.Icons.ARROW_BACK, on_click=cancel), - ft.Text("新規顧客登録", size=18, weight=ft.FontWeight.BOLD), - ]), - 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), - name_field, - ft.Container(height=10), - formal_name_field, - ft.Container(height=10), - address_field, - ft.Container(height=10), - phone_field, - ft.Container(height=20), - ft.Row([ - ft.Button("保存", on_click=save_customer, bgcolor=ft.Colors.BLUE_GREY_800, color=ft.Colors.WHITE), - ft.Button("キャンセル", on_click=cancel), - ], spacing=10), - ]), - padding=ft.padding.all(20), - expand=True, - ), - ]), - expand=True, - ) - - 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}") - - # 編集中の伝票があれば顧客を設定 - if self.editing_invoice: - self.editing_invoice.customer = customer - logging.info(f"編集中伝票に顧客を設定: {customer.formal_name}") - - # 顧客選択画面を閉じて元の画面に戻る - self.is_customer_picker_open = False - self.customer_search_query = "" - self.update_main_content() - - def submit_invoice_for_tax(self, invoice_uuid: str) -> bool: - """税務署提出済みフラグを設定""" - success = self.app_service.invoice.submit_to_tax_authority(invoice_uuid) - if success: - self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.update_main_content() - logging.info(f"税務署提出済み: {invoice_uuid}") - else: - logging.error(f"税務署提出失敗: {invoice_uuid}") - return success - 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}") - self.update_main_content() - 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_framework_demo.py b/app_framework_demo.py deleted file mode 100644 index 391a5ba..0000000 --- a/app_framework_demo.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -テキストエディタフレームワークデモ -再利用可能なコンポーネントの使用例 -""" - -import flet as ft -import sqlite3 -import signal -import sys -import logging -from components.text_editor import TextEditor, create_draft_editor, create_memo_editor, create_note_editor - -def main(page: ft.Page): - """メイン関数""" - try: - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS text_storage ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - category TEXT NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("データベース初期化完了") - - # ウィンドウ設定 - page.title = "テキストエディタフレームワークデモ" - page.window_width = 800 - page.window_height = 600 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # ナビゲーション設定 - current_editor = [0] - editors = [] - - # エディタ作成 - draft_editor = create_draft_editor(page) - memo_editor = create_memo_editor(page) - note_editor = create_note_editor(page) - - editors = [draft_editor, memo_editor, note_editor] - - # タブ切り替え - tabs = ft.Tabs( - selected_index=0, - tabs=[ - ft.Tab( - text="下書き", - content=draft_editor.build() - ), - ft.Tab( - text="メモ", - content=memo_editor.build() - ), - ft.Tab( - text="ノート", - content=note_editor.build() - ) - ], - expand=True - ) - - # ページ構築 - page.add( - ft.Column([ - ft.Text("テキストエディタフレームワーク", size=24, weight=ft.FontWeight.BOLD), - ft.Text("再利用可能なコンポーネントのデモ", size=16, color=ft.Colors.GREY_600), - ft.Divider(), - tabs - ], expand=True, spacing=10) - ) - - logging.info("テキストエディタフレームワーク起動完了") - print("🚀 テキストエディタフレームワーク起動完了") - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - print(f"❌ アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - import flet as ft - ft.run(main) diff --git a/app_hierarchical_product_master.py b/app_hierarchical_product_master.py deleted file mode 100644 index 72afd9e..0000000 --- a/app_hierarchical_product_master.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -階層構造商品マスターデモアプリケーション -入れ子構造とPDF巨大カッコ表示に対応 -""" - -import flet as ft -import signal -import sys -import logging -from components.hierarchical_product_master import create_hierarchical_product_master - -class HierarchicalProductMasterApp: - """階層構造商品マスターデモアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - self.signal_handler = signal_handler - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # ウィンドウ設定 - page.title = "階層構造商品マスター" - page.window_width = 1400 - page.window_height = 800 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # 階層商品マスター作成 - self.product_master = create_hierarchical_product_master(page) - - # ヘッダー - self.header = ft.Container( - content=ft.Column([ - ft.Row([ - ft.Icon( - ft.Icons.INVENTORY_2, - size=40, - color=ft.Colors.BLUE_900 - ), - ft.Column([ - ft.Text( - "階層構造商品マスター", - size=28, - weight=ft.FontWeight.BOLD, - color=ft.Colors.BLUE_900 - ), - ft.Text( - "入れ子構造とPDF巨大カッコ表示に対応した商品管理システム", - size=14, - color=ft.Colors.GREY_600 - ) - ], spacing=5) - ], alignment=ft.MainAxisAlignment.START), - ft.Divider(height=2, thickness=2) - ], spacing=10), - padding=20, - bgcolor=ft.Colors.BLUE_50, - border_radius=10, - margin=ft.Margin.only(bottom=20) - ) - - # 操作説明 - self.instructions = ft.Container( - content=ft.Column([ - ft.Text( - "操作方法", - size=16, - weight=ft.FontWeight.BOLD, - color=ft.Colors.BLUE_900 - ), - ft.Text("• 左側のツリーから商品を選択して編集", size=12), - ft.Text("• カテゴリの展開/折りたたみ:▶/▼アイコンをクリック", size=12), - ft.Text("• 新規追加:選択中のノードの子として追加", size=12), - ft.Text("• PDFプレビュー:階層構造をキャラクターベースで表示", size=12), - ft.Text("• 巨大カッコ:PDF出力時に階層を視覚的に表現", size=12), - ], spacing=5), - padding=15, - bgcolor=ft.Colors.GREY_50, - border_radius=10, - margin=ft.Margin.only(bottom=20) - ) - - # メインコンテナ - self.main_container = ft.Column([ - self.header, - self.instructions, - self.product_master.build() - ], expand=True, spacing=20) - - # ページに追加 - page.add( - ft.Container( - content=self.main_container, - padding=20, - bgcolor=ft.Colors.GREY_100, - expand=True - ) - ) - - logging.info("階層構造商品マスターアプリ起動完了") - print("🚀 階層構造商品マスターアプリ起動完了") - -def main(page: ft.Page): - """メイン関数""" - try: - app = HierarchicalProductMasterApp(page) - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_master_management.py b/app_master_management.py deleted file mode 100644 index bc58ef2..0000000 --- a/app_master_management.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -マスタ管理アプリケーション -統合的なマスタ管理機能を提供 -""" - -import flet as ft -import sqlite3 -import signal -import sys -import logging -from components.master_editor import ( - CustomerMasterEditor, ProductMasterEditor, SalesSlipMasterEditor, - create_customer_master, create_product_master, create_sales_slip_master -) - -class MasterManagementApp: - """マスタ管理アプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - ErrorHandler.current_page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - self._init_database() - - # ウィンドウ設定 - page.title = "マスタ管理システム" - page.window_width = 1000 - page.window_height = 700 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # マスタエディタ作成 - self.customer_editor = create_customer_master(page) - self.product_editor = create_product_master(page) - self.sales_slip_editor = create_sales_slip_master(page) - - # 現在のエディタ - current_editor = [0] - - # タブインターフェース - tabs = ft.Tabs( - selected_index=0, - tabs=[ - ft.Tab( - text="顧客マスタ", - content=self.customer_editor.build() - ), - ft.Tab( - text="商品マスタ", - content=self.product_editor.build() - ), - ft.Tab( - text="伝票マスタ", - content=self.sales_slip_editor.build() - ) - ], - expand=True - ) - - # ページ構築 - page.add( - ft.Column([ - ft.Text("マスタ管理システム", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900), - ft.Divider(), - ft.Text("各マスタデータの編集・管理が可能です", size=16), - ft.Divider(), - tabs - ], expand=True, spacing=15) - ) - - logging.info("マスタ管理システム起動完了") - print("🚀 マスタ管理システム起動完了") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各マスタテーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales_slips ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - customer_name TEXT NOT NULL, - items TEXT NOT NULL, - total_amount REAL NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("マスタデータベース初期化完了") - - except Exception as e: - logging.error(f"データベース初期化エラー: {e}") - -def main(page: ft.Page): - """メイン関数""" - try: - app = MasterManagementApp(page) - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_robust.py b/app_robust.py deleted file mode 100644 index cfa4434..0000000 --- a/app_robust.py +++ /dev/null @@ -1,681 +0,0 @@ -import flet as ft -import sqlite3 -import signal -import sys -import logging -import random -from datetime import datetime -from typing import List, Dict, Optional - -class ErrorHandler: - """グローバルエラーハンドラ""" - - @staticmethod - def handle_error(error: Exception, context: str = ""): - """エラーを一元処理""" - error_msg = f"{context}: {str(error)}" - logging.error(error_msg) - print(f"❌ {error_msg}") - - # SnackBarでユーザーに通知 - try: - # グローバルページ参照用 - if hasattr(ErrorHandler, 'current_page'): - ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) - except: - pass - - @staticmethod - def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): - """SnackBarを表示""" - try: - page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - page.snack_bar.open = True - page.update() - except: - pass - -class DummyDataGenerator: - """テスト用ダミーデータ生成""" - - @staticmethod - def generate_customers(count: int = 100) -> List[Dict]: - """ダミー顧客データ生成""" - first_names = ["田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本", "中村", "小林", "加藤"] - last_names = ["太郎", "次郎", "三郎", "花子", "美子", "健一", "恵子", "大輔", "由美", "翔太"] - - customers = [] - for i in range(count): - name = f"{random.choice(first_names)} {random.choice(last_names)}" - customers.append({ - 'id': i + 1, - 'name': name, - 'phone': f"090-{random.randint(1000, 9999)}-{random.randint(1000, 9999)}", - 'email': f"customer{i+1}@example.com", - 'address': f"東京都{random.choice(['渋谷区', '新宿区', '港区', '千代田区'])}{random.randint(1, 50)}-{random.randint(1, 10)}" - }) - return customers - - @staticmethod - def generate_products(count: int = 50) -> List[Dict]: - """ダミー商品データ生成""" - categories = ["電子機器", "衣料品", "食品", "書籍", "家具"] - products = [] - - for i in range(count): - category = random.choice(categories) - products.append({ - 'id': i + 1, - 'name': f"{category}{i+1}", - 'category': category, - 'price': random.randint(100, 50000), - 'stock': random.randint(0, 100) - }) - return products - - @staticmethod - def generate_sales(count: int = 200) -> List[Dict]: - """ダミー売上データ生成""" - customers = DummyDataGenerator.generate_customers(20) - products = DummyDataGenerator.generate_products(30) - - sales = [] - for i in range(count): - customer = random.choice(customers) - product = random.choice(products) - quantity = random.randint(1, 10) - - sales.append({ - 'id': i + 1, - 'customer_id': customer['id'], - 'customer_name': customer['name'], - 'product_id': product['id'], - 'product_name': product['name'], - 'quantity': quantity, - 'unit_price': product['price'], - 'total_price': quantity * product['price'], - 'date': datetime.now().strftime("%Y-%m-%d"), - 'created_at': datetime.now().isoformat() - }) - return sales - -class NavigationHistory: - """ナビゲーション履歴管理""" - - def __init__(self): - self.history: List[Dict] = [] - self.max_history = 10 - - def add_to_history(self, page_name: str, page_data: Dict = None): - """履歴に追加""" - history_item = { - 'page': page_name, - 'data': page_data, - 'timestamp': datetime.now().isoformat() - } - - # 重複を避ける - self.history = [item for item in self.history if item['page'] != page_name] - self.history.insert(0, history_item) - - # 履歴数を制限 - if len(self.history) > self.max_history: - self.history = self.history[:self.max_history] - - def get_last_page(self) -> Optional[Dict]: - """最後のページを取得""" - return self.history[0] if self.history else None - - def get_history(self) -> List[Dict]: - """履歴を取得""" - return self.history - -class SafePageManager: - """安全なページマネージャー""" - - def __init__(self, page: ft.Page): - self.page = page - self.current_page = None - self.navigation_history = NavigationHistory() - ErrorHandler.current_page = page # グローバル参照用 - - def safe_navigate(self, page_name: str, page_builder): - """安全なページ遷移""" - try: - # 現在のページ情報を保存 - current_data = {} - if self.current_page: - current_data = self._get_page_data() - - self.navigation_history.add_to_history(page_name, current_data) - - # 新しいページを構築 - page_instance = page_builder(self) - new_page = page_instance.build() - - # 安全なページ切り替え - self._safe_page_transition(new_page, page_name) - - except Exception as e: - ErrorHandler.handle_error(e, f"ページ遷移エラー ({page_name})") - - def _get_page_data(self) -> Dict: - """現在のページデータを取得""" - try: - if hasattr(self.current_page, 'get_data'): - return self.current_page.get_data() - return {} - except: - return {} - - def _safe_page_transition(self, new_page, page_name: str): - """安全なページ切り替え""" - try: - # 古いコンテンツをクリア - self.page.controls.clear() - - # 新しいコンテンツを追加 - self.page.add(new_page) - - # 現在のページを更新 - self.current_page = new_page - - # ページを更新 - self.page.update() - - logging.info(f"ページ遷移成功: {page_name}") - - except Exception as e: - ErrorHandler.handle_error(e, f"ページ表示エラー ({page_name})") - - def go_back(self): - """前のページに戻る""" - try: - history = self.navigation_history.get_history() - if len(history) > 1: - # 前のページに戻る - previous_page = history[1] - - # ページビルダーを呼び出し - if previous_page['page'] == 'dashboard': - self.safe_navigate('dashboard', DashboardPage) - elif previous_page['page'] == 'sales': - self.safe_navigate('sales', SalesPage) - elif previous_page['page'] == 'customers': - self.safe_navigate('customers', CustomerPage) - elif previous_page['page'] == 'products': - self.safe_navigate('products', ProductPage) - - # 履歴を更新 - self.navigation_history.history.pop(0) # 現在の履歴を削除 - - except Exception as e: - ErrorHandler.handle_error(e, "戻る処理エラー") - -class DashboardPage: - """ダッシュボードページ""" - - def __init__(self, page_manager): - self.page_manager = page_manager - - def build(self): - """ダッシュボードUI構築""" - try: - # 統計データ取得 - stats = self._get_statistics() - - return ft.Container( - content=ft.Column([ - ft.Text("ダッシュボード", size=24, weight=ft.FontWeight.BOLD), - ft.Divider(), - ft.Row([ - self._stat_card("総顧客数", stats['customers'], ft.Colors.BLUE), - self._stat_card("総商品数", stats['products'], ft.Colors.GREEN), - ], spacing=10), - ft.Row([ - self._stat_card("総売上件数", stats['sales'], ft.Colors.ORANGE), - self._stat_card("総売上高", f"¥{stats['total_sales']:,.0f}", ft.Colors.PURPLE), - ], spacing=10), - ]), - padding=20 - ) - except Exception as e: - ErrorHandler.handle_error(e, "ダッシュボード構築エラー") - return ft.Text("ダッシュボード読み込みエラー") - - def _stat_card(self, title: str, value: str, color: ft.Colors): - """統計カード作成""" - return ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Text(title, size=16, color=color), - ft.Text(value, size=20, weight=ft.FontWeight.BOLD), - ]), - padding=15 - ), - width=200 - ) - - def _get_statistics(self) -> Dict: - """統計データ取得""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各テーブルの件数取得 - cursor.execute("SELECT COUNT(*) FROM customers") - customers = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*) FROM products") - products = cursor.fetchone()[0] - - cursor.execute("SELECT COUNT(*), COALESCE(SUM(total_price), 0) FROM sales") - sales_result = cursor.fetchone() - sales_count = sales_result[0] - total_sales = sales_result[1] - - conn.close() - - return { - 'customers': customers, - 'products': products, - 'sales': sales_count, - 'total_sales': total_sales - } - except Exception as e: - ErrorHandler.handle_error(e, "統計データ取得エラー") - return {'customers': 0, 'products': 0, 'sales': 0, 'total_sales': 0} - - def get_data(self) -> Dict: - """ページデータ取得""" - return {'page': 'dashboard'} - -class SalesPage: - """売上管理ページ""" - - def __init__(self, page_manager): - self.page_manager = page_manager - - def build(self): - """売上管理UI構築""" - try: - return ft.Container( - content=ft.Column([ - ft.Text("売上管理", size=24, weight=ft.FontWeight.BOLD), - ft.Divider(), - ft.Row([ - ft.TextField(label="顧客名", width=200), - ft.TextField(label="商品名", width=200), - ft.TextField(label="金額", width=150), - ft.Button("追加", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE), - ]), - ft.Divider(), - ft.Text("売上一覧", size=18), - ft.Container( - content=ft.Column([], scroll=ft.ScrollMode.AUTO), - height=300 - ) - ]), - padding=20 - ) - except Exception as e: - ErrorHandler.handle_error(e, "売上管理ページ構築エラー") - return ft.Text("売上管理ページ読み込みエラー") - - def get_data(self) -> Dict: - """ページデータ取得""" - return {'page': 'sales'} - -class CustomerPage: - """顧客管理ページ""" - - def __init__(self, page_manager): - self.page_manager = page_manager - - def build(self): - """顧客管理UI構築""" - try: - return ft.Container( - content=ft.Column([ - ft.Text("顧客管理", size=24, weight=ft.FontWeight.BOLD), - ft.Divider(), - ft.Row([ - ft.Button("新規追加", bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE), - ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE), - ]), - ft.Divider(), - ft.Text("顧客一覧", size=18), - ft.Container( - content=ft.Column([], scroll=ft.ScrollMode.AUTO), - height=300 - ) - ]), - padding=20 - ) - except Exception as e: - ErrorHandler.handle_error(e, "顧客管理ページ構築エラー") - return ft.Text("顧客管理ページ読み込みエラー") - - def get_data(self) -> Dict: - """ページデータ取得""" - return {'page': 'customers'} - -class ProductPage: - """商品管理ページ""" - - def __init__(self, page_manager): - self.page_manager = page_manager - - def build(self): - """商品管理UI構築""" - try: - return ft.Container( - content=ft.Column([ - ft.Text("商品管理", size=24, weight=ft.FontWeight.BOLD), - ft.Divider(), - ft.Row([ - ft.Button("新規追加", bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE), - ft.Button("一括インポート", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE), - ]), - ft.Divider(), - ft.Text("商品一覧", size=18), - ft.Container( - content=ft.Column([], scroll=ft.ScrollMode.AUTO), - height=300 - ) - ]), - padding=20 - ) - except Exception as e: - ErrorHandler.handle_error(e, "商品管理ページ構築エラー") - return ft.Text("商品管理ページ読み込みエラー") - - def get_data(self) -> Dict: - """ページデータ取得""" - return {'page': 'products'} - -class SalesAssistantApp: - """メインアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - self.page_manager = SafePageManager(page) - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) - - # データベース初期化 - self._init_database() - self._generate_dummy_data() - - # ナビゲーションバー構築 - self._build_navigation() - - # 初期ページ表示 - self.page_manager.safe_navigate('dashboard', DashboardPage) - - def _signal_handler(self, signum, frame): - """シグナルハンドラ""" - print(f"\nシグナル {signum} を受信しました") - self._cleanup_resources() - sys.exit(0) - - def _cleanup_resources(self): - """リソースクリーンアップ""" - try: - logging.info("アプリケーション終了処理開始") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - except Exception as e: - logging.error(f"クリーンアップエラー: {e}") - print(f"❌ クリーンアップエラー: {e}") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各テーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_id INTEGER, - customer_name TEXT NOT NULL, - product_id INTEGER, - product_name TEXT NOT NULL, - quantity INTEGER NOT NULL, - unit_price REAL NOT NULL, - total_price REAL NOT NULL, - date TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (customer_id) REFERENCES customers(id), - FOREIGN KEY (product_id) REFERENCES products(id) - ) - ''') - - conn.commit() - conn.close() - logging.info("データベース初期化完了") - - except Exception as e: - ErrorHandler.handle_error(e, "データベース初期化エラー") - - def _generate_dummy_data(self): - """ダミーデータ生成""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 既存データチェック - cursor.execute("SELECT COUNT(*) FROM customers") - if cursor.fetchone()[0] == 0: - print("📊 ダミーデータを生成中...") - - # 顧客データ - customers = DummyDataGenerator.generate_customers(50) - for customer in customers: - cursor.execute(''' - INSERT INTO customers (name, phone, email, address) - VALUES (?, ?, ?, ?) - ''', (customer['name'], customer['phone'], customer['email'], customer['address'])) - - # 商品データ - products = DummyDataGenerator.generate_products(30) - for product in products: - cursor.execute(''' - INSERT INTO products (name, category, price, stock) - VALUES (?, ?, ?, ?) - ''', (product['name'], product['category'], product['price'], product['stock'])) - - # 売上データ - sales = DummyDataGenerator.generate_sales(100) - for sale in sales: - cursor.execute(''' - INSERT INTO sales (customer_id, customer_name, product_id, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ''', (sale['customer_id'], sale['customer_name'], sale['product_id'], - sale['product_name'], sale['quantity'], sale['unit_price'], - sale['total_price'], sale['date'])) - - conn.commit() - print("✅ ダミーデータ生成完了") - - conn.close() - - except Exception as e: - ErrorHandler.handle_error(e, "ダミーデータ生成エラー") - - def _build_navigation(self): - """ナビゲーションバー構築""" - try: - # ナビゲーションボタン - nav_buttons = [ - ft.ElevatedButton( - "ダッシュボード", - on_click=lambda _: self.page_manager.safe_navigate('dashboard', DashboardPage), - bgcolor=ft.Colors.BLUE, - color=ft.Colors.WHITE - ), - ft.ElevatedButton( - "売上管理", - on_click=lambda _: self.page_manager.safe_navigate('sales', SalesPage), - bgcolor=ft.Colors.GREEN, - color=ft.Colors.WHITE - ), - ft.ElevatedButton( - "顧客管理", - on_click=lambda _: self.page_manager.safe_navigate('customers', CustomerPage), - bgcolor=ft.Colors.ORANGE, - color=ft.Colors.WHITE - ), - ft.ElevatedButton( - "商品管理", - on_click=lambda _: self.page_manager.safe_navigate('products', ProductPage), - bgcolor=ft.Colors.PURPLE, - color=ft.Colors.WHITE - ), - ft.ElevatedButton( - "戻る", - on_click=lambda _: self.page_manager.go_back(), - bgcolor=ft.Colors.RED, - color=ft.Colors.WHITE - ) - ] - - # ナビゲーションバー - self.page.navigation_bar = ft.NavigationBar( - destinations=[ - ft.NavigationBarDestination( - icon=ft.Icons.DASHBOARD, - label="ダッシュボード" - ), - ft.NavigationBarDestination( - icon=ft.Icons.SHOPPING_CART, - label="売上" - ), - ft.NavigationBarDestination( - icon=ft.Icons.PEOPLE, - label="顧客" - ), - ft.NavigationBarDestination( - icon=ft.Icons.INVENTORY, - label="商品" - ) - ], - on_change=self._on_nav_change - ) - - # 代替ナビゲーション(NavigationBarが動かない場合) - self.page.add( - ft.Container( - content=ft.Row([ - ft.Button( - "ダッシュボード", - on_click=lambda _: self.page_manager.safe_navigate('dashboard', DashboardPage), - bgcolor=ft.Colors.BLUE, - color=ft.Colors.WHITE - ), - ft.Button( - "売上管理", - on_click=lambda _: self.page_manager.safe_navigate('sales', SalesPage), - bgcolor=ft.Colors.GREEN, - color=ft.Colors.WHITE - ), - ft.Button( - "顧客管理", - on_click=lambda _: self.page_manager.safe_navigate('customers', CustomerPage), - bgcolor=ft.Colors.ORANGE, - color=ft.Colors.WHITE - ), - ft.Button( - "商品管理", - on_click=lambda _: self.page_manager.safe_navigate('products', ProductPage), - bgcolor=ft.Colors.PURPLE, - color=ft.Colors.WHITE - ), - ft.Button( - "戻る", - on_click=lambda _: self.page_manager.go_back(), - bgcolor=ft.Colors.RED, - color=ft.Colors.WHITE - ) - ], spacing=5), - padding=10, - bgcolor=ft.Colors.GREY_100 - ) - ) - - except Exception as e: - ErrorHandler.handle_error(e, "ナビゲーション構築エラー") - - def _on_nav_change(self, e): - """ナビゲーション変更イベント""" - try: - index = e.control.selected_index - pages = [DashboardPage, SalesPage, CustomerPage, ProductPage] - page_names = ['dashboard', 'sales', 'customers', 'products'] - - if 0 <= index < len(pages): - self.page_manager.safe_navigate(page_names[index], pages[index]) - except Exception as ex: - ErrorHandler.handle_error(ex, "ナビゲーション変更エラー") - -def main(page: ft.Page): - """メイン関数""" - try: - # ウィンドウ設定 - page.title = "販売アシスト1号" - page.window_width = 800 - page.window_height = 600 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: SalesAssistantApp(page)._cleanup_resources() - - # アプリケーション起動 - app = SalesAssistantApp(page) - logging.info("アプリケーション起動完了") - print("🚀 頑健な販売アシスト1号起動完了") - - except Exception as e: - ErrorHandler.handle_error(e, "アプリケーション起動エラー") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_simple_working.py b/app_simple_working.py deleted file mode 100644 index eab0f55..0000000 --- a/app_simple_working.py +++ /dev/null @@ -1,377 +0,0 @@ -import flet as ft -import sqlite3 -import signal -import sys -import logging -import random -from datetime import datetime -from typing import List, Dict, Optional - -class ErrorHandler: - """グローバルエラーハンドラ""" - - @staticmethod - def handle_error(error: Exception, context: str = ""): - """エラーを一元処理""" - error_msg = f"{context}: {str(error)}" - logging.error(error_msg) - print(f"❌ {error_msg}") - - try: - if hasattr(ErrorHandler, 'current_page'): - ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) - except: - pass - - @staticmethod - def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): - """SnackBarを表示""" - try: - page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - page.snack_bar.open = True - page.update() - except: - pass - -class DummyDataGenerator: - """テスト用ダミーデータ生成""" - - @staticmethod - def generate_customers(count: int = 100) -> List[Dict]: - """ダミー顧客データ生成""" - first_names = ["田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本", "中村", "小林", "加藤"] - last_names = ["太郎", "次郎", "三郎", "花子", "美子", "健一", "恵子", "大輔", "由美", "翔太"] - - customers = [] - for i in range(count): - name = f"{random.choice(first_names)} {random.choice(last_names)}" - customers.append({ - 'id': i + 1, - 'name': name, - 'phone': f"090-{random.randint(1000, 9999)}-{random.randint(1000, 9999)}", - 'email': f"customer{i+1}@example.com", - 'address': f"東京都{random.choice(['渋谷区', '新宿区', '港区', '千代田区'])}{random.randint(1, 50)}-{random.randint(1, 10)}" - }) - return customers - - @staticmethod - def generate_products(count: int = 50) -> List[Dict]: - """ダミー商品データ生成""" - categories = ["電子機器", "衣料品", "食品", "書籍", "家具"] - products = [] - - for i in range(count): - category = random.choice(categories) - products.append({ - 'id': i + 1, - 'name': f"{category}{i+1}", - 'category': category, - 'price': random.randint(100, 50000), - 'stock': random.randint(0, 100) - }) - return products - - @staticmethod - def generate_sales(count: int = 200) -> List[Dict]: - """ダミー売上データ生成""" - customers = DummyDataGenerator.generate_customers(20) - products = DummyDataGenerator.generate_products(30) - - sales = [] - for i in range(count): - customer = random.choice(customers) - product = random.choice(products) - quantity = random.randint(1, 10) - - sales.append({ - 'id': i + 1, - 'customer_id': customer['id'], - 'customer_name': customer['name'], - 'product_id': product['id'], - 'product_name': product['name'], - 'quantity': quantity, - 'unit_price': product['price'], - 'total_price': quantity * product['price'], - 'date': datetime.now().strftime("%Y-%m-%d"), - 'created_at': datetime.now().isoformat() - }) - return sales - -def main(page: ft.Page): - """メイン関数""" - try: - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - def init_db(): - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_name TEXT NOT NULL, - product_name TEXT NOT NULL, - quantity INTEGER NOT NULL, - unit_price REAL NOT NULL, - total_price REAL NOT NULL, - date TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("データベース初期化完了") - - except Exception as e: - logging.error(f"データベース初期化エラー: {e}") - print(f"❌ データベース初期化エラー: {e}") - - # ダミーデータ生成 - def generate_dummy_data(): - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute("SELECT COUNT(*) FROM customers") - if cursor.fetchone()[0] == 0: - print("📊 ダミーデータを生成中...") - - # 顧客データ - customers = DummyDataGenerator.generate_customers(50) - for customer in customers: - cursor.execute(''' - INSERT INTO customers (name, phone, email, address) - VALUES (?, ?, ?, ?) - ''', (customer['name'], customer['phone'], customer['email'], customer['address'])) - - # 商品データ - products = DummyDataGenerator.generate_products(30) - for product in products: - cursor.execute(''' - INSERT INTO products (name, category, price, stock) - VALUES (?, ?, ?, ?) - ''', (product['name'], product['category'], product['price'], product['stock'])) - - # 売上データ - sales = DummyDataGenerator.generate_sales(100) - for sale in sales: - cursor.execute(''' - INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?) - ''', (sale['customer_name'], sale['product_name'], sale['quantity'], - sale['unit_price'], sale['total_price'], sale['date'])) - - conn.commit() - print("✅ ダミーデータ生成完了") - - conn.close() - - except Exception as e: - logging.error(f"ダミーデータ生成エラー: {e}") - print(f"❌ ダミーデータ生成エラー: {e}") - - # ウィンドウ設定 - page.title = "販売アシスト1号 (シンプル版)" - page.window_width = 800 - page.window_height = 600 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # データベース初期化とダミーデータ生成 - init_db() - generate_dummy_data() - - # 現在のページインデックス - current_page_index = [0] - - # UI要素 - title = ft.Text("販売アシスト1号", size=24, weight=ft.FontWeight.BOLD) - - # フォーム要素 - customer_tf = ft.TextField(label="顧客名", width=200, autofocus=True) - product_tf = ft.TextField(label="商品名", width=200) - amount_tf = ft.TextField(label="金額", width=150, keyboard_type=ft.KeyboardType.NUMBER) - - # ボタン - add_btn = ft.Button("追加", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - - # 売上一覧 - sales_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300) - - # 操作説明 - instructions = ft.Text( - "操作方法: TABでフォーカス移動、SPACE/ENTERで追加、1-4でページ遷移", - size=12, - color=ft.Colors.GREY_600 - ) - - def load_sales(): - """売上データ読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT customer_name, product_name, total_price, date - FROM sales - ORDER BY created_at DESC - LIMIT 20 - ''') - sales = cursor.fetchall() - conn.close() - - # リストを更新 - sales_list.controls.clear() - for sale in sales: - customer, product, amount, date = sale - sales_list.controls.append( - ft.Text(f"{date}: {customer} - {product}: ¥{amount:,.0f}") - ) - - page.update() - - except Exception as e: - logging.error(f"売上データ読み込みエラー: {e}") - - def add_sale(e): - """売上データ追加""" - if customer_tf.value and product_tf.value and amount_tf.value: - try: - # 保存前に値を取得 - customer_val = customer_tf.value - product_val = product_tf.value - amount_val = float(amount_tf.value) - - # データベースに保存 - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date) - VALUES (?, ?, ?, ?, ?, ?) - ''', (customer_val, product_val, 1, amount_val, amount_val, datetime.now().strftime("%Y-%m-%d"))) - - conn.commit() - conn.close() - - # フィールドをクリア - customer_tf.value = "" - product_tf.value = "" - amount_tf.value = "" - - # リスト更新 - load_sales() - - # 成功メッセージ - page.snack_bar = ft.SnackBar( - content=ft.Text("保存しました"), - bgcolor=ft.Colors.GREEN - ) - page.snack_bar.open = True - page.update() - - logging.info(f"売上データ追加: {customer_val} {product_val} {amount_val}") - - except Exception as ex: - logging.error(f"売上データ保存エラー: {ex}") - page.snack_bar = ft.SnackBar( - content=ft.Text("エラーが発生しました"), - bgcolor=ft.Colors.RED - ) - page.snack_bar.open = True - page.update() - - # キーボードイベントハンドラ - def on_keyboard(e: ft.KeyboardEvent): - # SPACEまたはENTERで追加 - if e.key == " " or e.key == "Enter": - add_sale(None) - # 数字キーでページ遷移(今回はシンプルに) - elif e.key == "1": - print("ダッシュボードに遷移します") - elif e.key == "2": - print("売上管理に遷移します") - elif e.key == "3": - print("顧客管理に遷移します") - elif e.key == "4": - print("商品管理に遷移します") - # ESCで終了 - elif e.key == "Escape": - signal_handler(0, None) - - # キーボードイベント設定 - page.on_keyboard_event = on_keyboard - - # 初期データ読み込み - load_sales() - - # ページ構築 - page.add( - title, - ft.Divider(), - ft.Text("売上登録", size=18, weight=ft.FontWeight.BOLD), - ft.Row([customer_tf, product_tf, amount_tf, add_btn]), - ft.Divider(), - ft.Text("売上一覧", size=18), - instructions, - ft.Divider(), - sales_list - ) - - logging.info("アプリケーション起動完了") - print("🚀 シンプル版販売アシスト1号起動完了") - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - print(f"❌ アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_slip_adaptive.py b/app_slip_adaptive.py deleted file mode 100644 index 96491c3..0000000 --- a/app_slip_adaptive.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -業態適応型伝票システム -事業者の業態に応じて最適なフォームを提供 -""" - -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 deleted file mode 100644 index 82d0a0a..0000000 --- a/app_slip_explorer.py +++ /dev/null @@ -1,416 +0,0 @@ -""" -伝票エクスプローラー -伝票の検索・閲覧・管理を直感的に行う -土地勘を持たせるための視覚的ナビゲーション -""" - -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_framework_demo.py b/app_slip_framework_demo.py deleted file mode 100644 index d605dfd..0000000 --- a/app_slip_framework_demo.py +++ /dev/null @@ -1,95 +0,0 @@ -""" -伝票入力フレームワークデモアプリケーション -""" - -import flet as ft -import sqlite3 -import signal -import sys -import logging -from components.slip_entry_framework import SlipEntryFramework, create_slip_entry_framework - -class SlipFrameworkDemoApp: - """伝票入力フレームワークデモアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - self._init_database() - - # ウィンドウ設定 - page.title = "伝票入力フレームワークデモ" - page.window_width = 1000 - page.window_height = 700 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # 伝票入力フレームワーク作成 - self.slip_framework = create_slip_entry_framework(page) - - # ページ構築 - page.add( - self.slip_framework.build() - ) - - logging.info("伝票入力フレームワークデモ起動完了") - print("🚀 伝票入力フレームワークデモ起動完了") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 伝票テーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS slips ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - theme_name TEXT NOT NULL, - items_data TEXT NOT NULL, - total_amount REAL NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("伝票データベース初期化完了") - - except Exception as e: - logging.error(f"データベース初期化エラー: {e}") - -def main(page: ft.Page): - """メイン関数""" - try: - app = SlipFrameworkDemoApp(page) - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_slip_interactive.py b/app_slip_interactive.py deleted file mode 100644 index e9e624e..0000000 --- a/app_slip_interactive.py +++ /dev/null @@ -1,412 +0,0 @@ -""" -インタラクティブ伝票ビューア -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/app_text_editor.py b/app_text_editor.py deleted file mode 100644 index 2f44099..0000000 --- a/app_text_editor.py +++ /dev/null @@ -1,427 +0,0 @@ -import flet as ft -import sqlite3 -import signal -import sys -import logging -import random -from datetime import datetime -from typing import List, Dict, Optional - -class ErrorHandler: - """グローバルエラーハンドラ""" - - @staticmethod - def handle_error(error: Exception, context: str = ""): - """エラーを一元処理""" - error_msg = f"{context}: {str(error)}" - logging.error(error_msg) - print(f"❌ {error_msg}") - - try: - if hasattr(ErrorHandler, 'current_page'): - ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) - except: - pass - - @staticmethod - def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): - """SnackBarを表示""" - try: - page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - page.snack_bar.open = True - page.update() - except: - pass - -class TextEditor: - """テキストエディタ""" - - def __init__(self, page: ft.Page): - self.page = page - - # UI部品 - self.title = ft.Text("下書きエディタ", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - - # テキストエリア - self.text_area = ft.TextField( - label="下書き内容", - multiline=True, - min_lines=10, - max_lines=50, - value="", - autofocus=True, - width=600, - height=400 - ) - - # ボタン群 - self.save_btn = ft.Button( - "保存", - on_click=self.save_draft, - bgcolor=ft.Colors.GREEN, - color=ft.Colors.WHITE - ) - - self.clear_btn = ft.Button( - "クリア", - on_click=self.clear_text, - bgcolor=ft.Colors.ORANGE, - color=ft.Colors.WHITE - ) - - self.load_btn = ft.Button( - "読込", - on_click=self.load_draft, - bgcolor=ft.Colors.BLUE, - color=ft.Colors.WHITE - ) - - self.delete_btn = ft.Button( - "削除", - on_click=self.delete_draft, - bgcolor=ft.Colors.RED, - color=ft.Colors.WHITE - ) - - # 下書きリスト - self.draft_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=200) - - # 操作説明 - self.instructions = ft.Text( - "操作方法: TABでフォーカス移動、SPACE/ENTERで保存、マウスクリックでもボタン操作可能", - size=12, - color=ft.Colors.GREY_600 - ) - - # データ読み込み - self.load_drafts() - - def build(self): - """UIを構築して返す""" - return ft.Column([ - self.title, - ft.Divider(), - ft.Row([self.save_btn, self.clear_btn, self.load_btn, self.delete_btn], spacing=10), - ft.Divider(), - self.text_area, - ft.Divider(), - ft.Text("下書きリスト", size=18, weight=ft.FontWeight.BOLD), - self.instructions, - self.draft_list - ], expand=True, spacing=15) - - def save_draft(self, e): - """下書きを保存""" - if self.text_area.value.strip(): - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS drafts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - INSERT INTO drafts (title, content) - VALUES (?, ?) - ''', (f"下書き_{datetime.now().strftime('%Y%m%d_%H%M%S')}", self.text_area.value)) - - conn.commit() - conn.close() - - # 成功メッセージ - ErrorHandler.show_snackbar(self.page, "下書きを保存しました", ft.Colors.GREEN) - logging.info(f"下書き保存: {len(self.text_area.value)} 文字") - - # リスト更新 - self.load_drafts() - - except Exception as ex: - ErrorHandler.handle_error(ex, "下書き保存エラー") - - def clear_text(self, e): - """テキストをクリア""" - self.text_area.value = "" - self.page.update() - logging.info("テキストをクリア") - - def load_draft(self, e): - """下書きを読み込み""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS drafts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - SELECT id, title, content, created_at - FROM drafts - ORDER BY created_at DESC - LIMIT 10 - ''') - drafts = cursor.fetchall() - conn.close() - - if drafts: - # テキストエリアに設定 - self.text_area.value = drafts[0][2] # content - - # リスト更新 - self.draft_list.controls.clear() - for draft in drafts: - draft_id, title, content, created_at = draft - - # 下書きカード - draft_card = ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Row([ - ft.Text(f"ID: {draft_id}", size=12, color=ft.Colors.GREY_600), - ft.Text(f"作成: {created_at}", size=12, color=ft.Colors.GREY_600) - ], spacing=5), - ft.Text(title, weight=ft.FontWeight.BOLD, size=14), - ft.Text(content[:100] + "..." if len(content) > 100 else content, size=12), - ]), - padding=10, - width=400 - ), - margin=ft.margin.only(bottom=5) - ) - - # 読込ボタン - load_btn = ft.Button( - "読込", - on_click=lambda _, did=draft_id: self.load_specific_draft(draft_id), - bgcolor=ft.Colors.BLUE, - color=ft.Colors.WHITE, - width=80 - ) - - self.draft_list.controls.append( - ft.Row([draft_card, load_btn], alignment=ft.MainAxisAlignment.SPACE_BETWEEN) - ) - - self.page.update() - logging.info(f"下書き読込完了: {len(drafts)} 件") - - else: - ErrorHandler.show_snackbar(self.page, "下書きがありません", ft.Colors.ORANGE) - - except Exception as ex: - ErrorHandler.handle_error(ex, "下書き読込エラー") - - def load_specific_draft(self, draft_id): - """特定の下書きを読み込む""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - SELECT content - FROM drafts - WHERE id = ? - ''', (draft_id,)) - - result = cursor.fetchone() - conn.close() - - if result: - self.text_area.value = result[0] - ErrorHandler.show_snackbar(self.page, f"下書き #{draft_id} を読み込みました", ft.Colors.GREEN) - logging.info(f"下書き読込: ID={draft_id}") - else: - ErrorHandler.show_snackbar(self.page, "下書きが見つかりません", ft.Colors.RED) - - except Exception as ex: - ErrorHandler.handle_error(ex, "下書き読込エラー") - - def delete_draft(self, e): - """下書きを削除""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - DELETE FROM drafts - WHERE id = (SELECT id FROM drafts ORDER BY created_at DESC LIMIT 1) - ''') - - conn.commit() - conn.close() - - # リスト更新 - self.load_drafts() - - ErrorHandler.show_snackbar(self.page, "下書きを削除しました", ft.Colors.GREEN) - logging.info("下書き削除完了") - - except Exception as ex: - ErrorHandler.handle_error(ex, "下書き削除エラー") - - def load_drafts(self): - """下書きリストを読み込む""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS drafts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - SELECT id, title, content, created_at - FROM drafts - ORDER BY created_at DESC - LIMIT 10 - ''') - drafts = cursor.fetchall() - conn.close() - - # リストを更新 - self.draft_list.controls.clear() - for draft in drafts: - draft_id, title, content, created_at = draft - - # 下書きカード - draft_card = ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Row([ - ft.Text(f"ID: {draft_id}", size=12, color=ft.Colors.GREY_600), - ft.Text(f"作成: {created_at}", size=12, color=ft.Colors.GREY_600) - ], spacing=5), - ft.Text(title, weight=ft.FontWeight.BOLD, size=14), - ft.Text(content[:100] + "..." if len(content) > 100 else content, size=12), - ]), - padding=10, - width=400 - ), - margin=ft.margin.only(bottom=5) - ) - - # 読込ボタン - load_btn = ft.Button( - "読込", - on_click=lambda _, did=draft_id: self.load_specific_draft(draft_id), - bgcolor=ft.Colors.BLUE, - color=ft.Colors.WHITE, - width=80 - ) - - self.draft_list.controls.append( - ft.Row([draft_card, load_btn], alignment=ft.MainAxisAlignment.SPACE_BETWEEN) - ) - - self.page.update() - - except Exception as e: - ErrorHandler.handle_error(e, "下書きリスト読込エラー") - -class SimpleApp: - """シンプルなアプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - ErrorHandler.current_page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - self._init_database() - - # テキストエディタ作成 - self.text_editor = TextEditor(page) - - # ページ構築 - page.add( - self.text_editor.build() - ) - - logging.info("テキストエディタ起動完了") - print("🚀 テキストエディタ起動完了") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 下書きテーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS drafts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("データベース初期化完了") - - except Exception as e: - ErrorHandler.handle_error(e, "データベース初期化エラー") - -def main(page: ft.Page): - """メイン関数""" - try: - # ウィンドウ設定 - page.title = "テキストエディタ" - page.window_width = 800 - page.window_height = 600 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: SimpleApp(page)._cleanup_resources() - - # アプリケーション起動 - app = SimpleApp(page) - - except Exception as e: - ErrorHandler.handle_error(e, "アプリケーション起動エラー") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_theme_master.py b/app_theme_master.py deleted file mode 100644 index d274d95..0000000 --- a/app_theme_master.py +++ /dev/null @@ -1,521 +0,0 @@ -""" -テーマ対応マスタ管理アプリケーション -UI統一 + SQLiteテーマ管理 -""" - -import flet as ft -import sqlite3 -import signal -import sys -import logging -from datetime import datetime -from typing import List, Dict, Optional - -class ThemeManager: - """テーマ管理クラス""" - - def __init__(self): - self.themes = {} - self.current_theme = None - self._load_themes() - - def _load_themes(self): - """テーマ情報をSQLiteから読み込む""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # テーマテーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS themes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - title TEXT NOT NULL, - table_name TEXT NOT NULL, - fields TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # デフォルトテーマを挿入 - default_themes = [ - { - 'name': 'customers', - 'title': '顧客マスタ', - 'table_name': 'customers', - 'fields': '[{"name": "name", "label": "顧客名", "width": 200, "required": true}, {"name": "phone", "label": "電話番号", "width": 200}, {"name": "email", "label": "メールアドレス", "width": 250}, {"name": "address", "label": "住所", "width": 300}]' - }, - { - 'name': 'products', - 'title': '商品マスタ', - 'table_name': 'products', - 'fields': '[{"name": "name", "label": "商品名", "width": 200, "required": true}, {"name": "category", "label": "カテゴリ", "width": 150}, {"name": "price", "label": "価格", "width": 100, "keyboard_type": "number", "required": true}, {"name": "stock", "label": "在庫数", "width": 100, "keyboard_type": "number"}]' - }, - { - 'name': 'sales_slips', - 'title': '伝票マスタ', - 'table_name': 'sales_slips', - 'fields': '[{"name": "title", "label": "伝票タイトル", "width": 300, "required": true}, {"name": "customer_name", "label": "顧客名", "width": 200, "required": true}, {"name": "items", "label": "明細", "width": 400, "keyboard_type": "multiline", "required": true}, {"name": "total_amount", "label": "合計金額", "width": 150, "keyboard_type": "number", "required": true}]' - } - ] - - # デフォルトテーマがなければ挿入 - cursor.execute("SELECT COUNT(*) FROM themes") - if cursor.fetchone()[0] == 0: - for theme in default_themes: - cursor.execute(''' - INSERT INTO themes (name, title, table_name, fields) - VALUES (?, ?, ?, ?) - ''', (theme['name'], theme['title'], theme['table_name'], theme['fields'])) - conn.commit() - - # テーマを読み込み - cursor.execute("SELECT * FROM themes ORDER BY id") - themes_data = cursor.fetchall() - - for theme_data in themes_data: - theme_id, name, title, table_name, fields_json, created_at = theme_data - self.themes[name] = { - 'id': theme_id, - 'title': title, - 'table_name': table_name, - 'fields': eval(fields_json) # JSONをPythonオブジェクトに変換 - } - - conn.close() - logging.info(f"テーマ読込完了: {len(self.themes)}個") - - except Exception as e: - logging.error(f"テーマ読込エラー: {e}") - - def get_theme_names(self) -> List[str]: - """テーマ名リストを取得""" - return list(self.themes.keys()) - - def get_theme(self, name: str) -> Dict: - """指定されたテーマを取得""" - return self.themes.get(name, {}) - -class UniversalMasterEditor: - """汎用マスタ編集コンポーネント""" - - def __init__(self, page: ft.Page): - self.page = page - self.theme_manager = ThemeManager() - - # 現在のテーマ - self.current_theme_name = 'customers' - self.current_theme = self.theme_manager.get_theme(self.current_theme_name) - self.current_data = [] - self.editing_id = None - - # UI部品 - self.title = ft.Text("", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) - self.theme_dropdown = ft.Dropdown( - label="マスタ選択", - options=[], - value="customers", - on_change=self.change_theme - ) - - # フォーム - self.form_fields = ft.Column([], spacing=10) - - # ボタン群 - self.add_btn = ft.Button("追加", on_click=self.add_new, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) - self.save_btn = ft.Button("保存", on_click=self.save_data, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE) - self.delete_btn = ft.Button("削除", on_click=self.delete_selected, bgcolor=ft.Colors.RED, color=ft.Colors.WHITE) - self.clear_btn = ft.Button("クリア", on_click=self.clear_form, bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE) - - # データリスト - self.data_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300) - - # 操作説明 - self.instructions = ft.Text( - "操作方法: マスタを選択してデータを編集・追加・削除", - size=12, - color=ft.Colors.GREY_600 - ) - - # 初期化 - self._update_theme_dropdown() - self._build_form() - self._load_data() - - def _update_theme_dropdown(self): - """テーマドロップダウンを更新""" - theme_names = self.theme_manager.get_theme_names() - self.theme_dropdown.options = [ - ft.dropdown.Option(theme['title'], name) - for name, theme in self.theme_manager.themes.items() - ] - self.page.update() - - def change_theme(self, e): - """テーマを切り替え""" - self.current_theme_name = e.control.value - self.current_theme = self.theme_manager.get_theme(self.current_theme_name) - self.editing_id = None - self.title.value = self.current_theme['title'] - self._build_form() - self._load_data() - self.page.update() - - def _build_form(self): - """入力フォームを構築""" - self.form_fields.controls.clear() - - for field in self.current_theme['fields']: - keyboard_type = ft.KeyboardType.TEXT - if field.get('keyboard_type') == 'number': - keyboard_type = ft.KeyboardType.NUMBER - elif field.get('keyboard_type') == 'multiline': - keyboard_type = ft.KeyboardType.MULTILINE - - field_control = ft.TextField( - label=field['label'], - value='', - width=field['width'], - keyboard_type=keyboard_type - ) - self.form_fields.controls.append(field_control) - - self.page.update() - - def _load_data(self): - """データを読み込む""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - table_name = self.current_theme['table_name'] - cursor.execute(f''' - SELECT * FROM {table_name} - ORDER BY id - ''') - - self.current_data = cursor.fetchall() - conn.close() - - # データリストを更新 - self.data_list.controls.clear() - for item in self.current_data: - item_id = item[0] - item_data = dict(zip([col[0] for col in cursor.description], item)) - - # データ行 - row_controls = [] - for field in self.current_theme['fields']: - field_name = field['name'] - value = item_data.get(field_name, '') - - row_controls.append( - ft.Text(f"{field['label']}: {value}", size=12) - ) - - # 操作ボタン - row_controls.extend([ - ft.Button( - "編集", - on_click=lambda _, did=item_id: self.edit_item(item_id), - bgcolor=ft.Colors.ORANGE, - color=ft.Colors.WHITE, - width=60 - ), - ft.Button( - "削除", - on_click=lambda _, did=item_id: self.delete_item(item_id), - bgcolor=ft.Colors.RED, - color=ft.Colors.WHITE, - width=60 - ) - ]) - - # データカード - data_card = ft.Card( - content=ft.Container( - content=ft.Column(row_controls), - padding=10 - ), - margin=ft.margin.only(bottom=5) - ) - - self.data_list.controls.append(data_card) - - self.page.update() - - except Exception as e: - logging.error(f"{self.current_theme_name}データ読込エラー: {e}") - - def save_data(self, e): - """データを保存""" - try: - # フォームデータを収集 - form_data = {} - for i, field in enumerate(self.current_theme['fields']): - field_control = self.form_fields.controls[i] - form_data[field['name']] = field_control.value - - # 必須項目チェック - if field.get('required', False) and not field_control.value.strip(): - self._show_snackbar(f"{field['label']}は必須項目です", ft.Colors.RED) - return - - # データベースに保存 - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - table_name = self.current_theme['table_name'] - - if self.editing_id: - # 更新 - columns = ', '.join([f"{key} = ?" for key in form_data.keys()]) - cursor.execute(f''' - UPDATE {table_name} - SET {columns} - WHERE id = ? - ''', tuple(form_data.values()) + (self.editing_id,)) - - self._show_snackbar("データを更新しました", ft.Colors.GREEN) - logging.info(f"{table_name}データ更新完了: ID={self.editing_id}") - else: - # 新規追加 - columns = ', '.join([field['name'] for field in self.current_theme['fields']]) - placeholders = ', '.join(['?' for _ in self.current_theme['fields']]) - - cursor.execute(f''' - INSERT INTO {table_name} ({columns}) - VALUES ({placeholders}) - ''', tuple(form_data.values())) - - self._show_snackbar("データを追加しました", ft.Colors.GREEN) - logging.info(f"{table_name}データ追加完了") - - conn.commit() - conn.close() - - # フォームをクリア - self.clear_form(None) - - # データ再読み込み - self._load_data() - - except Exception as e: - logging.error(f"{self.current_theme_name}データ保存エラー: {e}") - self._show_snackbar("保存エラー", ft.Colors.RED) - - def add_new(self, e): - """新規データを追加""" - self.editing_id = None - self.clear_form(None) - self.form_fields.controls[0].focus() - logging.info(f"{self.current_theme_name}新規追加モード") - - def edit_item(self, item_id): - """データを編集""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - table_name = self.current_theme['table_name'] - cursor.execute(f''' - SELECT * FROM {table_name} - WHERE id = ? - ''', (item_id,)) - - result = cursor.fetchone() - conn.close() - - if result: - self.editing_id = item_id - - # フォームにデータを設定 - for i, field in enumerate(self.current_theme['fields']): - field_control = self.form_fields.controls[i] - field_control.value = result[i+1] if i+1 < len(result) else '' - - self.page.update() - logging.info(f"{table_name}編集モード: ID={item_id}") - - except Exception as e: - logging.error(f"{self.current_theme_name}編集エラー: {e}") - - def delete_item(self, item_id): - """データを削除""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - table_name = self.current_theme['table_name'] - cursor.execute(f''' - DELETE FROM {table_name} - WHERE id = ? - ''', (item_id,)) - - conn.commit() - conn.close() - - # データ再読み込み - self._load_data() - - self._show_snackbar("データを削除しました", ft.Colors.GREEN) - logging.info(f"{table_name}削除完了: ID={item_id}") - - except Exception as e: - logging.error(f"{self.current_theme_name}削除エラー: {e}") - self._show_snackbar("削除エラー", ft.Colors.RED) - - def delete_selected(self, e): - """選択中のデータを削除""" - if self.editing_id: - self.delete_item(self.editing_id) - else: - self._show_snackbar("削除対象が選択されていません", ft.Colors.ORANGE) - - def clear_form(self, e): - """フォームをクリア""" - for field_control in self.form_fields.controls: - field_control.value = '' - - self.editing_id = None - self.page.update() - logging.info("フォームをクリア") - - def _show_snackbar(self, message: str, color: ft.Colors): - """SnackBarを表示""" - try: - self.page.snack_bar = ft.SnackBar( - content=ft.Text(message), - bgcolor=color - ) - self.page.snack_bar.open = True - self.page.update() - except: - pass - - def build(self): - """UIを構築して返す""" - return ft.Column([ - ft.Row([ - self.title, - self.theme_dropdown - ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), - ft.Divider(), - ft.Text("入力フォーム", size=18, weight=ft.FontWeight.BOLD), - self.form_fields, - ft.Row([self.add_btn, self.save_btn, self.delete_btn, self.clear_btn], spacing=10), - ft.Divider(), - ft.Text(f"{self.current_theme['title']}一覧", size=18, weight=ft.FontWeight.BOLD), - self.instructions, - self.data_list - ], expand=True, spacing=15) - -class ThemeMasterApp: - """テーマ対応マスタ管理アプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - self._init_database() - - # ウィンドウ設定 - page.title = "テーマ対応マスタ管理システム" - page.window_width = 1000 - page.window_height = 700 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # テーマ対応マスタエディタ作成 - self.theme_editor = UniversalMasterEditor(page) - - # ページ構築 - page.add( - self.theme_editor.build() - ) - - logging.info("テーマ対応マスタ管理システム起動完了") - print("🚀 テーマ対応マスタ管理システム起動完了") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各マスタテーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales_slips ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - customer_name TEXT NOT NULL, - items TEXT NOT NULL, - total_amount REAL NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("マスタデータベース初期化完了") - - except Exception as e: - logging.error(f"データベース初期化エラー: {e}") - -def main(page: ft.Page): - """メイン関数""" - try: - app = ThemeMasterApp(page) - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/app_universal_master.py b/app_universal_master.py deleted file mode 100644 index dc8c6ed..0000000 --- a/app_universal_master.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -汎用マスタ管理アプリケーション -統合的なマスタ管理機能を提供 -""" - -import flet as ft -import sqlite3 -import signal -import sys -import logging -from components.universal_master_editor import UniversalMasterEditor, create_universal_master_editor - -class UniversalMasterApp: - """汎用マスタ管理アプリケーション""" - - def __init__(self, page: ft.Page): - self.page = page - ErrorHandler.current_page = page - - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - def signal_handler(signum, frame): - print(f"\nシグナル {signum} を受信しました") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - # データベース初期化 - self._init_database() - - # ウィンドウ設定 - page.title = "汎用マスタ管理システム" - page.window_width = 1000 - page.window_height = 700 - page.theme_mode = ft.ThemeMode.LIGHT - - # ウィンドウクローズイベント - page.on_window_close = lambda _: signal_handler(0, None) - - # 汎用マスタエディタ作成 - self.universal_editor = create_universal_master_editor(page) - - # ページ構築 - page.add( - self.universal_editor.build() - ) - - logging.info("汎用マスタ管理システム起動完了") - print("🚀 汎用マスタ管理システム起動完了") - - def _init_database(self): - """データベース初期化""" - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # 各マスタテーブル作成 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS customers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - address TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS products ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - category TEXT, - price REAL NOT NULL, - stock INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales_slips ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT NOT NULL, - customer_name TEXT NOT NULL, - items TEXT NOT NULL, - total_amount REAL NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - logging.info("マスタデータベース初期化完了") - - except Exception as e: - logging.error(f"データベース初期化エラー: {e}") - -def main(page: ft.Page): - """メイン関数""" - try: - app = UniversalMasterApp(page) - - except Exception as e: - logging.error(f"アプリケーション起動エラー: {e}") - -if __name__ == "__main__": - ft.run(main) diff --git a/build.py b/build.py index 66cc317..a0acb66 100644 --- a/build.py +++ b/build.py @@ -1,40 +1,48 @@ """Androidビルド用スクリプト""" + import subprocess import sys -import os -def build_android(): - """販売アシスト1号をAndroidアプリとしてビルド""" - - print("🚀 販売アシスト1号 Androidビルド開始...") - - # FletでAndroidビルド + +def build_android(target: str = "apk") -> bool: + """販売アシスト1号をAndroidアプリとしてビルド。targetはapk/aab。""" + if target not in {"apk", "aab"}: + print(f"❌ 未対応ターゲット: {target}(apk / aab を指定してください)") + return False + + print(f"🚀 販売アシスト1号 Androidビルド開始... target={target}") + try: - result = subprocess.run([ - sys.executable, "-m", "flet", "pack", "main.py", - "--android", - "--name", "販売アシスト1号", - "--package-name", "com.sales.assistant1", - "--icon", "icon.png" # アイコンがあれば - ], check=True, capture_output=True, text=True) - + result = subprocess.run( + ["flet", "build", target, ".", "--module-name", "main", "--yes"], + check=True, + capture_output=True, + text=True, + ) print("✅ ビルド成功!") - print(result.stdout) - + if result.stdout: + print(result.stdout) + return True + except FileNotFoundError: + print("❌ flet コマンドが見つかりません") + print(" source .venv/bin/activate && pip install flet") + return False except subprocess.CalledProcessError as e: print("❌ ビルド失敗:") - print(e.stderr) + if e.stdout: + print(e.stdout) + if e.stderr: + print(e.stderr) return False - except FileNotFoundError: - print("❌ Fletがインストールされていません") - print("pip install flet を実行してください") - return False - - return True + if __name__ == "__main__": - if build_android(): - print("\n🎉 販売アシスト1号のAndroidビルドが完了しました!") - print("生成されたAPKファイルをAndroid端末にインストールしてください") + target = "apk" + if len(sys.argv) > 1: + target = sys.argv[1].strip().lower() + + if build_android(target): + print(f"\n🎉 Androidビルド完了: {target}") + print("生成物は build/ または dist/ 配下を確認してください") else: print("\n💥 ビルドに失敗しました") diff --git a/components/editor_framework.py b/components/editor_framework.py new file mode 100644 index 0000000..4afaad1 --- /dev/null +++ b/components/editor_framework.py @@ -0,0 +1,196 @@ +"""編集系画面で再利用するEditorフレームワーク(最小版)。""" + +from dataclasses import dataclass +from typing import Callable, List + +import flet as ft + +from models.invoice_models import InvoiceItem + + +@dataclass +class ValidationResult: + """編集内容の検証結果。""" + + ok: bool + errors: List[str] + + +def normalize_invoice_items(items: List[InvoiceItem]) -> List[InvoiceItem]: + """保存前に明細を正規化(空行除去・数値補正)。""" + normalized: List[InvoiceItem] = [] + + for item in items: + description = (item.description or "").strip() + quantity = int(item.quantity or 0) + unit_price = int(item.unit_price or 0) + + if not description and quantity == 0 and unit_price == 0: + continue + + if quantity <= 0: + quantity = 1 + + normalized.append( + InvoiceItem( + description=description, + quantity=quantity, + unit_price=unit_price, + is_discount=bool(getattr(item, "is_discount", False)), + product_id=getattr(item, "product_id", None), + ) + ) + + return normalized + + +def validate_invoice_items(items: List[InvoiceItem]) -> ValidationResult: + """明細の最小バリデーション。""" + errors: List[str] = [] + + if not items: + errors.append("明細を1行以上入力してください") + return ValidationResult(ok=False, errors=errors) + + for idx, item in enumerate(items, start=1): + if not (item.description or "").strip(): + errors.append(f"{idx}行目: 商品名を入力してください") + if int(item.quantity or 0) <= 0: + errors.append(f"{idx}行目: 数量は1以上で入力してください") + if int(item.unit_price or 0) < 0: + errors.append(f"{idx}行目: 単価は0以上で入力してください") + + return ValidationResult(ok=len(errors) == 0, errors=errors) + + +def build_invoice_items_view_table(items: List[InvoiceItem]) -> ft.Column: + """表示モードの明細テーブル。""" + header_row = ft.Row( + [ + ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), + ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), + ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), + ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), + ft.Container(width=35), + ] + ) + + data_rows = [] + for item in items: + data_rows.append( + ft.Row( + [ + ft.Text(item.description, size=12, expand=True), + ft.Text(str(item.quantity), size=12, width=35, text_align=ft.TextAlign.RIGHT), + ft.Text(f"¥{item.unit_price:,}", size=12, width=70, text_align=ft.TextAlign.RIGHT), + ft.Text( + f"¥{item.subtotal:,}", + size=12, + weight=ft.FontWeight.BOLD, + width=70, + text_align=ft.TextAlign.RIGHT, + ), + ft.Container(width=35), + ] + ) + ) + + return ft.Column( + [ + header_row, + ft.Divider(height=1, color=ft.Colors.GREY_400), + ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), + ] + ) + + +def build_invoice_items_edit_table( + items: List[InvoiceItem], + is_locked: bool, + on_update_field: Callable[[int, str, str], None], + on_delete_row: Callable[[int], None], +) -> ft.Column: + """編集モードの明細テーブル。""" + header_row = ft.Row( + [ + ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), + ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), + ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), + ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), + ft.Container(width=35), + ] + ) + + data_rows = [] + for i, item in enumerate(items): + product_field = ft.TextField( + value=item.description, + text_size=12, + height=28, + expand=True, + border=ft.border.all(1, ft.Colors.BLUE_200), + bgcolor=ft.Colors.WHITE, + content_padding=ft.padding.all(5), + on_change=lambda e, idx=i: on_update_field(idx, "description", e.control.value), + ) + + quantity_field = ft.TextField( + value=str(item.quantity), + text_size=12, + height=28, + width=35, + text_align=ft.TextAlign.RIGHT, + border=ft.border.all(1, ft.Colors.BLUE_200), + bgcolor=ft.Colors.WHITE, + content_padding=ft.padding.all(5), + on_change=lambda e, idx=i: on_update_field(idx, "quantity", e.control.value), + keyboard_type=ft.KeyboardType.NUMBER, + ) + + unit_price_field = ft.TextField( + value=f"{item.unit_price:,}", + text_size=12, + height=28, + width=70, + text_align=ft.TextAlign.RIGHT, + border=ft.border.all(1, ft.Colors.BLUE_200), + bgcolor=ft.Colors.WHITE, + content_padding=ft.padding.all(5), + on_change=lambda e, idx=i: on_update_field(idx, "unit_price", e.control.value.replace(",", "")), + keyboard_type=ft.KeyboardType.NUMBER, + ) + + delete_button = ft.IconButton( + ft.Icons.DELETE_OUTLINE, + tooltip="行を削除", + icon_color=ft.Colors.RED_600, + disabled=is_locked, + icon_size=16, + on_click=lambda _, idx=i: on_delete_row(idx), + ) + + data_rows.append( + ft.Row( + [ + product_field, + quantity_field, + unit_price_field, + ft.Text( + f"¥{item.subtotal:,}", + size=12, + weight=ft.FontWeight.BOLD, + width=70, + text_align=ft.TextAlign.RIGHT, + ), + delete_button, + ] + ) + ) + + return ft.Column( + [ + header_row, + ft.Divider(height=1, color=ft.Colors.GREY_400), + ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), + ] + ) diff --git a/components/explorer_framework.py b/components/explorer_framework.py new file mode 100644 index 0000000..1cfeebd --- /dev/null +++ b/components/explorer_framework.py @@ -0,0 +1,56 @@ +"""一覧系画面で再利用するExplorerフレームワーク(最小版)。""" + +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Optional, Tuple + + +EXPLORER_PERIODS = { + "7d": "直近7日", + "30d": "直近30日", + "3m": "直近3ヶ月", + "1y": "直近1年", + "all": "全期間", +} + + +EXPLORER_SORTS = { + "date": "日付", + "invoice_number": "伝票番号", + "customer_name": "顧客名", + "document_type": "種別", + "updated_at": "更新日時", +} + + +@dataclass +class ExplorerQueryState: + """Explorerの検索状態。""" + + query: str = "" + period_key: str = "3m" + sort_key: str = "date" + sort_desc: bool = True + include_offsets: bool = False + limit: int = 50 + offset: int = 0 + + +def to_date_range(period_key: str, now: Optional[datetime] = None) -> Tuple[Optional[str], Optional[str]]: + """期間キーからISO形式の日付範囲を返す。""" + now = now or datetime.now() + + if period_key == "all": + return None, None + if period_key == "7d": + start = now - timedelta(days=7) + elif period_key == "30d": + start = now - timedelta(days=30) + elif period_key == "3m": + start = now - timedelta(days=90) + elif period_key == "1y": + start = now - timedelta(days=365) + else: + start = now - timedelta(days=90) + + return start.replace(microsecond=0).isoformat(), now.replace(microsecond=0).isoformat() diff --git a/components/slip_entry_framework.py b/components/slip_entry_framework.py index 133f709..e9ce5ce 100644 --- a/components/slip_entry_framework.py +++ b/components/slip_entry_framework.py @@ -193,7 +193,7 @@ class SlipEntryFramework: self.theme_dropdown = ft.Dropdown( label="表示形式", options=[], - on_change=self.change_theme + on_select=self.change_theme ) # 入力フォーム diff --git a/components/universal_master_editor.py b/components/universal_master_editor.py index 6166228..fb35d26 100644 --- a/components/universal_master_editor.py +++ b/components/universal_master_editor.py @@ -9,6 +9,8 @@ import logging from datetime import datetime from typing import List, Dict, Optional, Callable +from components.explorer_framework import ExplorerQueryState + class UniversalMasterEditor: """汎用マスタ編集コンポーネント""" @@ -32,9 +34,8 @@ class UniversalMasterEditor: 'table_name': 'products', 'fields': [ {'name': 'name', 'label': '商品名', 'width': 200, 'required': True}, - {'name': 'category', 'label': 'カテゴリ', 'width': 150}, - {'name': 'price', 'label': '価格', 'width': 100, 'keyboard_type': ft.KeyboardType.NUMBER, 'required': True}, - {'name': 'stock', 'label': '在庫数', 'width': 100, 'keyboard_type': ft.KeyboardType.NUMBER} + {'name': 'unit_price', 'label': '単価', 'width': 120, 'keyboard_type': ft.KeyboardType.NUMBER, 'required': True}, + {'name': 'description', 'label': '説明', 'width': 320} ] }, 'sales_slips': { @@ -53,6 +54,14 @@ class UniversalMasterEditor: self.current_master = 'customers' self.current_data = [] self.editing_id = None + self.explorer_state = ExplorerQueryState(period_key="all", sort_key="id", limit=20) + + # 並び順(マスタ共通) + self.sort_candidates = { + "id": "ID", + "created_at": "作成日時", + "updated_at": "更新日時", + } # UI部品 self.title = ft.Text("", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) @@ -64,11 +73,37 @@ class UniversalMasterEditor: ft.dropdown.Option("伝票マスタ", "sales_slips") ], value="customers", - on_change=self.change_master + on_select=self.change_master ) # フォーム self.form_fields = ft.Column([], spacing=10) + + # Explorer操作 + self.search_field = ft.TextField( + label="検索", + hint_text="名称・説明などで検索", + prefix_icon=ft.Icons.SEARCH, + on_change=self.on_search_change, + dense=True, + expand=True, + ) + self.sort_dropdown = ft.Dropdown( + label="ソート", + options=[], + value="id", + on_select=self.on_sort_change, + width=170, + dense=True, + ) + self.sort_dir_btn = ft.IconButton( + icon=ft.Icons.ARROW_DOWNWARD, + tooltip="並び順切替", + on_click=self.toggle_sort_direction, + ) + self.prev_page_btn = ft.TextButton("◀ 前", on_click=self.prev_page) + self.next_page_btn = ft.TextButton("次 ▶", on_click=self.next_page) + self.page_status = ft.Text("", size=12, color=ft.Colors.BLUE_GREY_600) # ボタン群 self.add_btn = ft.Button("追加", on_click=self.add_new, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE) @@ -88,14 +123,74 @@ class UniversalMasterEditor: # 初期化 self._build_form() + self._refresh_sort_options() self._load_data() def change_master(self, e): """マスタを切り替え""" self.current_master = e.control.value self.editing_id = None + self.explorer_state.query = "" + self.explorer_state.offset = 0 + self.explorer_state.sort_key = "id" + self.explorer_state.sort_desc = True self.title.value = self.masters[self.current_master]['title'] + self.search_field.value = "" self._build_form() + self._refresh_sort_options() + self._load_data() + self.page.update() + + def _refresh_sort_options(self): + """現在マスタに合わせてソート候補を更新。""" + candidates = dict(self.sort_candidates) + for field in self.masters[self.current_master]["fields"]: + candidates.setdefault(field["name"], field["label"]) + + self.sort_dropdown.options = [ + ft.dropdown.Option(key, label) for key, label in candidates.items() + ] + + if self.explorer_state.sort_key not in candidates: + self.explorer_state.sort_key = "id" + + self.sort_dropdown.value = self.explorer_state.sort_key + self.sort_dir_btn.icon = ( + ft.Icons.ARROW_DOWNWARD if self.explorer_state.sort_desc else ft.Icons.ARROW_UPWARD + ) + + def on_search_change(self, e): + self.explorer_state.query = (e.control.value or "").strip() + self.explorer_state.offset = 0 + self._load_data() + self.page.update() + + def on_sort_change(self, e): + self.explorer_state.sort_key = e.control.value or "id" + self.explorer_state.offset = 0 + self._load_data() + self.page.update() + + def toggle_sort_direction(self, _): + self.explorer_state.sort_desc = not self.explorer_state.sort_desc + self.sort_dir_btn.icon = ( + ft.Icons.ARROW_DOWNWARD if self.explorer_state.sort_desc else ft.Icons.ARROW_UPWARD + ) + self.explorer_state.offset = 0 + self._load_data() + self.page.update() + + def prev_page(self, _): + if self.explorer_state.offset <= 0: + return + self.explorer_state.offset = max(0, self.explorer_state.offset - self.explorer_state.limit) + self._load_data() + self.page.update() + + def next_page(self, _): + if len(self.current_data) < self.explorer_state.limit: + return + self.explorer_state.offset += self.explorer_state.limit self._load_data() self.page.update() @@ -121,19 +216,43 @@ class UniversalMasterEditor: cursor = conn.cursor() table_name = self.masters[self.current_master]['table_name'] - cursor.execute(f''' + sort_key = self.explorer_state.sort_key or "id" + sort_direction = "DESC" if self.explorer_state.sort_desc else "ASC" + + allowed_sorts = {"id", "created_at", "updated_at"} + allowed_sorts.update(field["name"] for field in self.masters[self.current_master]["fields"]) + if sort_key not in allowed_sorts: + sort_key = "id" + + search_columns = [field["name"] for field in self.masters[self.current_master]["fields"]] + query = (self.explorer_state.query or "").strip() + + params = [] + where = "1=1" + if query: + like = f"%{query}%" + where = " OR ".join([f"{col} LIKE ?" for col in search_columns]) + where = f"({where})" + params.extend([like for _ in search_columns]) + + sql = f''' SELECT * FROM {table_name} - ORDER BY id - ''') - + WHERE {where} + ORDER BY {sort_key} {sort_direction}, id DESC + LIMIT ? OFFSET ? + ''' + params.extend([int(self.explorer_state.limit), int(self.explorer_state.offset)]) + + cursor.execute(sql, tuple(params)) self.current_data = cursor.fetchall() + col_names = [col[0] for col in cursor.description] conn.close() # データリストを更新 self.data_list.controls.clear() for item in self.current_data: item_id = item[0] - item_data = dict(zip([col[0] for col in cursor.description], item)) + item_data = dict(zip(col_names, item)) # データ行 row_controls = [] @@ -173,6 +292,10 @@ class UniversalMasterEditor: ) self.data_list.controls.append(data_card) + + self.page_status.value = ( + f"表示件数: {len(self.current_data)} / offset={self.explorer_state.offset} / limit={self.explorer_state.limit}" + ) self.page.update() @@ -186,10 +309,22 @@ class UniversalMasterEditor: form_data = {} for i, field in enumerate(self.masters[self.current_master]['fields']): field_control = self.form_fields.controls[i] - form_data[field['name']] = field_control.value + value = (field_control.value or "").strip() + + if field.get("keyboard_type") == ft.KeyboardType.NUMBER: + normalized = value.replace(",", "") + if normalized == "": + normalized = "0" + try: + value = str(int(float(normalized))) + except ValueError: + self._show_snackbar(f"{field['label']}は数値で入力してください", ft.Colors.RED) + return + + form_data[field['name']] = value # 必須項目チェック - if field.get('required', False) and not field_control.value.strip(): + if field.get('required', False) and not value: self._show_snackbar(f"{field['label']}は必須項目です", ft.Colors.RED) return @@ -339,6 +474,24 @@ class UniversalMasterEditor: ft.Divider(), ft.Text(f"{self.masters[self.current_master]['title']}一覧", size=18, weight=ft.FontWeight.BOLD), self.instructions, + ft.Container( + content=ft.Column([ + ft.Row([ + self.search_field, + self.sort_dropdown, + self.sort_dir_btn, + ], spacing=8), + ft.Row([ + self.page_status, + ft.Container(expand=True), + self.prev_page_btn, + self.next_page_btn, + ]), + ]), + padding=ft.padding.all(8), + bgcolor=ft.Colors.BLUE_GREY_50, + border_radius=8, + ), self.data_list ], expand=True, spacing=15) diff --git a/debug_test.py b/debug_test.py deleted file mode 100644 index ed5b5ff..0000000 --- a/debug_test.py +++ /dev/null @@ -1,9 +0,0 @@ -import flet as ft - -def main(page: ft.Page): - page.title = "テスト" - page.add(ft.Text("Hello World!")) - page.add(ft.ElevatedButton("クリック", on_click=lambda e: print("クリックされた"))) - -if __name__ == "__main__": - ft.app(target=main) diff --git a/flutter.参考/.gitignore b/flutter.参考/.gitignore deleted file mode 100644 index 3a91c4a..0000000 --- a/flutter.参考/.gitignore +++ /dev/null @@ -1,58 +0,0 @@ -# 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 deleted file mode 100644 index 26d3e69..0000000 --- a/flutter.参考/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# 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 deleted file mode 100644 index fb65c06..0000000 --- a/flutter.参考/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 225c63e..0000000 --- a/flutter.参考/aiderのお仕事.md +++ /dev/null @@ -1,9 +0,0 @@ -既存の 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 deleted file mode 100644 index 0d29021..0000000 --- a/flutter.参考/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# 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 deleted file mode 100644 index be3943c..0000000 --- a/flutter.参考/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 9c178e2..0000000 --- a/flutter.参考/android/app/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 399f698..0000000 --- a/flutter.参考/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/flutter.参考/android/app/src/main/AndroidManifest.xml b/flutter.参考/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 29a2967..0000000 --- a/flutter.参考/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index b2b225e..0000000 --- a/flutter.参考/android/app/src/main/kotlin/com/example/gemi_invoice/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index f74085f..0000000 --- a/flutter.参考/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/flutter.参考/android/app/src/main/res/drawable/launch_background.xml b/flutter.参考/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f..0000000 --- a/flutter.参考/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - 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 deleted file mode 100644 index db77bb4..0000000 Binary files a/flutter.参考/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null 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 deleted file mode 100644 index 17987b7..0000000 Binary files a/flutter.参考/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null 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 deleted file mode 100644 index 09d4391..0000000 Binary files a/flutter.参考/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null 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 deleted file mode 100644 index d5f1c8d..0000000 Binary files a/flutter.参考/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null 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 deleted file mode 100644 index 4d6372e..0000000 Binary files a/flutter.参考/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null 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 deleted file mode 100644 index 06952be..0000000 --- a/flutter.参考/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/flutter.参考/android/app/src/main/res/values/styles.xml b/flutter.参考/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef88..0000000 --- a/flutter.参考/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/flutter.参考/android/app/src/profile/AndroidManifest.xml b/flutter.参考/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f698..0000000 --- a/flutter.参考/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/flutter.参考/android/build.gradle.kts b/flutter.参考/android/build.gradle.kts deleted file mode 100644 index dbee657..0000000 --- a/flutter.参考/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index fbee1d8..0000000 --- a/flutter.参考/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index ca7fe06..0000000 --- a/flutter.参考/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100755 index 811b9c0..0000000 Binary files a/flutter.参考/assets/fonts/ipaexg.ttf and /dev/null differ diff --git a/flutter.参考/collectfluttercode.py b/flutter.参考/collectfluttercode.py deleted file mode 100644 index 33b606a..0000000 --- a/flutter.参考/collectfluttercode.py +++ /dev/null @@ -1,41 +0,0 @@ -# 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 deleted file mode 100644 index 7b9fe82..0000000 --- a/flutter.参考/dir.txt +++ /dev/null @@ -1,1005 +0,0 @@ -.: -合計 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 deleted file mode 100644 index ab653dc..0000000 --- a/flutter.参考/flutter_bundle_for_ai.txt +++ /dev/null @@ -1,2953 +0,0 @@ - -# ========================================== -# 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 deleted file mode 100644 index 7a7f987..0000000 --- a/flutter.参考/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/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 deleted file mode 100644 index 1dc6cf7..0000000 --- a/flutter.参考/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - 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 deleted file mode 100644 index 592ceee..0000000 --- a/flutter.参考/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/flutter.参考/ios/Flutter/Release.xcconfig b/flutter.参考/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/flutter.参考/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/flutter.参考/ios/Runner.xcodeproj/project.pbxproj b/flutter.参考/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 16463df..0000000 --- a/flutter.参考/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,616 +0,0 @@ -// !$*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 deleted file mode 100644 index 18d9810..0000000 --- a/flutter.参考/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/flutter.参考/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter.参考/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/flutter.参考/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/flutter.参考/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/flutter.参考/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/flutter.参考/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/flutter.参考/ios/Runner/AppDelegate.swift b/flutter.参考/ios/Runner/AppDelegate.swift deleted file mode 100644 index 6266644..0000000 --- a/flutter.参考/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index d36b1fa..0000000 --- a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "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 deleted file mode 100644 index dc9ada4..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null 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 deleted file mode 100644 index 7353c41..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null 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 deleted file mode 100644 index 797d452..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null 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 deleted file mode 100644 index 6ed2d93..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null 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 deleted file mode 100644 index 4cd7b00..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null 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 deleted file mode 100644 index fe73094..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null 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 deleted file mode 100644 index 321773c..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null 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 deleted file mode 100644 index 797d452..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null 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 deleted file mode 100644 index 502f463..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null 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 deleted file mode 100644 index 0ec3034..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null 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 deleted file mode 100644 index 0ec3034..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null 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 deleted file mode 100644 index e9f5fea..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null 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 deleted file mode 100644 index 84ac32a..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null 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 deleted file mode 100644 index 8953cba..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null 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 deleted file mode 100644 index 0467bf1..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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 deleted file mode 100644 index 9da19ea..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null 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 deleted file mode 100644 index 9da19ea..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null 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 deleted file mode 100644 index 9da19ea..0000000 Binary files a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/flutter.参考/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index f2e259c..0000000 --- a/flutter.参考/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter.参考/ios/Runner/Base.lproj/Main.storyboard b/flutter.参考/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/flutter.参考/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter.参考/ios/Runner/Info.plist b/flutter.参考/ios/Runner/Info.plist deleted file mode 100644 index fbb675b..0000000 --- a/flutter.参考/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - 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 deleted file mode 100644 index 308a2a5..0000000 --- a/flutter.参考/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/flutter.参考/ios/RunnerTests/RunnerTests.swift b/flutter.参考/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/flutter.参考/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 0fa8b9d..0000000 --- a/flutter.参考/lib/data/product_master.dart +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index 42acd54..0000000 --- a/flutter.参考/lib/main.dart +++ /dev/null @@ -1,145 +0,0 @@ -// 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 deleted file mode 100644 index 244a702..0000000 --- a/flutter.参考/lib/main.dart.org +++ /dev/null @@ -1,122 +0,0 @@ -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 deleted file mode 100644 index 79017cc..0000000 --- a/flutter.参考/lib/models/company_model.dart +++ /dev/null @@ -1,106 +0,0 @@ -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 deleted file mode 100644 index 2197344..0000000 --- a/flutter.参考/lib/models/customer_model.dart +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index 21c1335..0000000 --- a/flutter.参考/lib/models/invoice_models.dart +++ /dev/null @@ -1,180 +0,0 @@ -// 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 deleted file mode 100644 index dc98c80..0000000 --- a/flutter.参考/lib/screens/company_editor_screen.dart +++ /dev/null @@ -1,206 +0,0 @@ -// 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 deleted file mode 100644 index 5f37e3c..0000000 --- a/flutter.参考/lib/screens/customer_picker_modal.dart +++ /dev/null @@ -1,308 +0,0 @@ -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 deleted file mode 100644 index 674812a..0000000 --- a/flutter.参考/lib/screens/invoice_detail_page.dart +++ /dev/null @@ -1,249 +0,0 @@ -// 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 deleted file mode 100644 index 090da0b..0000000 --- a/flutter.参考/lib/screens/invoice_history_screen.dart +++ /dev/null @@ -1,186 +0,0 @@ -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 deleted file mode 100644 index b6ebd47..0000000 --- a/flutter.参考/lib/screens/invoice_input_screen.dart +++ /dev/null @@ -1,255 +0,0 @@ -// 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 deleted file mode 100644 index e85ff30..0000000 --- a/flutter.参考/lib/screens/product_picker_modal.dart +++ /dev/null @@ -1,274 +0,0 @@ -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 deleted file mode 100644 index 416be05..0000000 --- a/flutter.参考/lib/services/invoice_repository.dart +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index d51d86e..0000000 --- a/flutter.参考/lib/services/master_repository.dart +++ /dev/null @@ -1,150 +0,0 @@ -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 deleted file mode 100644 index 150a3c5..0000000 --- a/flutter.参考/lib/services/pdf_generator.dart +++ /dev/null @@ -1,295 +0,0 @@ -// 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 deleted file mode 100644 index d3896c9..0000000 --- a/flutter.参考/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/flutter.参考/linux/CMakeLists.txt b/flutter.参考/linux/CMakeLists.txt deleted file mode 100644 index 043f06d..0000000 --- a/flutter.参考/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# 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 deleted file mode 100644 index d5bd016..0000000 --- a/flutter.参考/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# 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 deleted file mode 100644 index 2dccc22..0000000 --- a/flutter.参考/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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 deleted file mode 100644 index e0f0a47..0000000 --- a/flutter.参考/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// 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 deleted file mode 100644 index 45f2369..0000000 --- a/flutter.参考/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# 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 deleted file mode 100644 index e97dabc..0000000 --- a/flutter.参考/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index e7c5c54..0000000 --- a/flutter.参考/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#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 deleted file mode 100644 index e2f8c9f..0000000 --- a/flutter.参考/linux/runner/my_application.cc +++ /dev/null @@ -1,148 +0,0 @@ -#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 deleted file mode 100644 index db16367..0000000 --- a/flutter.参考/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#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 deleted file mode 100644 index 746adbb..0000000 --- a/flutter.参考/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index c2efd0b..0000000 --- a/flutter.参考/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter.参考/macos/Flutter/Flutter-Release.xcconfig b/flutter.参考/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/flutter.参考/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter.参考/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter.参考/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 0413654..0000000 --- a/flutter.参考/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// 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 deleted file mode 100644 index b289e68..0000000 --- a/flutter.参考/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*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 deleted file mode 100644 index 18d9810..0000000 --- a/flutter.参考/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/flutter.参考/macos/Runner.xcworkspace/contents.xcworkspacedata b/flutter.参考/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/flutter.参考/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/flutter.参考/macos/Runner/AppDelegate.swift b/flutter.参考/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761..0000000 --- a/flutter.参考/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index a2ec33f..0000000 --- a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "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 deleted file mode 100644 index 82b6f9d..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null 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 deleted file mode 100644 index 13b35eb..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null 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 deleted file mode 100644 index 0a3f5fa..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null 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 deleted file mode 100644 index bdb5722..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null 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 deleted file mode 100644 index f083318..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null 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 deleted file mode 100644 index 326c0e7..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null 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 deleted file mode 100644 index 2f1632c..0000000 Binary files a/flutter.参考/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/flutter.参考/macos/Runner/Base.lproj/MainMenu.xib b/flutter.参考/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a..0000000 --- a/flutter.参考/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/flutter.参考/macos/Runner/Configs/AppInfo.xcconfig b/flutter.参考/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 169cc61..0000000 --- a/flutter.参考/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index 36b0fd9..0000000 --- a/flutter.参考/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/flutter.参考/macos/Runner/Configs/Release.xcconfig b/flutter.参考/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/flutter.参考/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/flutter.参考/macos/Runner/Configs/Warnings.xcconfig b/flutter.参考/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/flutter.参考/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index dddb8a3..0000000 --- a/flutter.参考/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - 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 deleted file mode 100644 index 4789daa..0000000 --- a/flutter.参考/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - 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 deleted file mode 100644 index 3cc05eb..0000000 --- a/flutter.参考/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 852fa1a..0000000 --- a/flutter.参考/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/flutter.参考/macos/RunnerTests/RunnerTests.swift b/flutter.参考/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1..0000000 --- a/flutter.参考/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index a920070..0000000 --- a/flutter.参考/pubspec.lock +++ /dev/null @@ -1,698 +0,0 @@ -# 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 deleted file mode 100644 index dcb3fd1..0000000 --- a/flutter.参考/pubspec.yaml +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index ca6227d..0000000 Binary files a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-07-465_com.example.gemi_invoice.jpg and /dev/null 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 deleted file mode 100644 index 01b74a6..0000000 Binary files a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-14-020_com.example.gemi_invoice.jpg and /dev/null 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 deleted file mode 100644 index a8d4f65..0000000 Binary files a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-23-357_com.example.gemi_invoice.jpg and /dev/null 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 deleted file mode 100644 index 74abef6..0000000 Binary files a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-35-940_com.example.gemi_invoice.jpg and /dev/null 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 deleted file mode 100644 index a3780b6..0000000 Binary files a/flutter.参考/screenshot/Screenshot_2026-02-05-16-18-44-960_com.example.gemi_invoice.jpg and /dev/null 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 deleted file mode 100644 index 36bb4e3..0000000 Binary files a/flutter.参考/screenshot/Screenshot_2026-02-05-16-19-07-838_com.example.gemi_invoice.jpg and /dev/null differ diff --git a/flutter.参考/test/widget_test.dart b/flutter.参考/test/widget_test.dart deleted file mode 100644 index 1363b77..0000000 --- a/flutter.参考/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// 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 deleted file mode 100644 index 8aaa46a..0000000 Binary files a/flutter.参考/web/favicon.png and /dev/null differ diff --git a/flutter.参考/web/index.html b/flutter.参考/web/index.html deleted file mode 100644 index 501e2b2..0000000 --- a/flutter.参考/web/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - gemi_invoice - - - - - - diff --git a/flutter.参考/windows/.gitignore b/flutter.参考/windows/.gitignore deleted file mode 100644 index d492d0d..0000000 --- a/flutter.参考/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100644 index ef3769b..0000000 --- a/flutter.参考/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# 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 deleted file mode 100644 index 903f489..0000000 --- a/flutter.参考/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# 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 deleted file mode 100644 index cb63ddc..0000000 --- a/flutter.参考/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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 deleted file mode 100644 index dc139d8..0000000 --- a/flutter.参考/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// 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 deleted file mode 100644 index 0f8c9e2..0000000 --- a/flutter.参考/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,27 +0,0 @@ -# -# 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 deleted file mode 100644 index 394917c..0000000 --- a/flutter.参考/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index 894c14a..0000000 --- a/flutter.参考/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// 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 deleted file mode 100644 index 955ee30..0000000 --- a/flutter.参考/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#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 deleted file mode 100644 index 6da0652..0000000 --- a/flutter.参考/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#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 deleted file mode 100644 index f8aaf28..0000000 --- a/flutter.参考/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#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 deleted file mode 100644 index 66a65d1..0000000 --- a/flutter.参考/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{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 deleted file mode 100644 index c04e20c..0000000 Binary files a/flutter.参考/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/flutter.参考/windows/runner/runner.exe.manifest b/flutter.参考/windows/runner/runner.exe.manifest deleted file mode 100644 index 153653e..0000000 --- a/flutter.参考/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,14 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - diff --git a/flutter.参考/windows/runner/utils.cpp b/flutter.参考/windows/runner/utils.cpp deleted file mode 100644 index 3a0b465..0000000 --- a/flutter.参考/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#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 deleted file mode 100644 index 3879d54..0000000 --- a/flutter.参考/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#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 deleted file mode 100644 index 60608d0..0000000 --- a/flutter.参考/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#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 deleted file mode 100644 index e901dde..0000000 --- a/flutter.参考/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#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 deleted file mode 100644 index aa46134..0000000 --- a/flutter.参考/データモデルのテンプレ.md +++ /dev/null @@ -1,8 +0,0 @@ -// 将来、Odooの account.move と紐付けるための最小構成 -class InvoiceModel { - final String localUuid; // 端末で生成する唯一無二のID - final String? odooId; // Odooに書き込まれたら返ってくるID(最初はnull) - final DateTime createdAt; // 発行日 - final List items; // 明細 - // ... -} diff --git a/main.py b/main.py index 6ebad84..ed6d4d4 100644 --- a/main.py +++ b/main.py @@ -7,10 +7,23 @@ import flet as ft import signal import sys import logging +import sqlite3 from datetime import datetime from typing import List, Dict, Optional from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer, InvoiceItem from components.customer_picker import CustomerPickerModal +from components.explorer_framework import ( + ExplorerQueryState, + EXPLORER_PERIODS, + EXPLORER_SORTS, + to_date_range, +) +from components.editor_framework import normalize_invoice_items, validate_invoice_items +from components.editor_framework import ( + build_invoice_items_view_table, + build_invoice_items_edit_table, +) +from components.universal_master_editor import UniversalMasterEditor from services.app_service import AppService # ロギング設定 @@ -37,7 +50,7 @@ class AppBar(ft.Container): """標準化されたアプリケーションヘッダー""" def __init__(self, title: str, show_back: bool = False, show_edit: bool = False, - on_back=None, on_edit=None, page=None): + on_back=None, on_edit=None, page=None, action_icon=None, action_tooltip: str = "編集"): super().__init__() self.title = title self.show_back = show_back @@ -45,6 +58,8 @@ class AppBar(ft.Container): self.on_back = on_back self.on_edit = on_edit self.page_ref = page # page_refとして保存 + self.action_icon = action_icon or ft.Icons.EDIT + self.action_tooltip = action_tooltip self.bgcolor = ft.Colors.BLUE_GREY_50 self.padding = ft.Padding.symmetric(horizontal=16, vertical=8) @@ -86,9 +101,9 @@ class AppBar(ft.Container): if self.show_edit: controls.append( ft.IconButton( - icon=ft.Icons.EDIT, + icon=self.action_icon, icon_color=ft.Colors.BLUE_GREY_700, - tooltip="編集", + tooltip=self.action_tooltip, on_click=self.on_edit if self.on_edit else None ) ) @@ -118,6 +133,43 @@ class FlutterStyleDashboard: self.show_offsets = False self.chain_verify_result = None self.is_new_customer_form_open = False + self.master_editor: Optional[UniversalMasterEditor] = None + self.explorer_state = ExplorerQueryState( + period_key="3m", + include_offsets=self.show_offsets, + limit=50, + offset=0, + ) + + # 保存後遷移設定(True: 詳細に留まる, False: 一覧へ戻る) + self.stay_on_detail_after_save = True + self.invoice_card_theme = { + "page_bg": "#F3F2FB", + "card_bg": ft.Colors.WHITE, + "card_radius": 18, + "shadow": ft.BoxShadow( + blur_radius=16, + spread_radius=0, + color="#D5D8F0", + offset=ft.Offset(0, 6), + ), + "icon_fg": ft.Colors.WHITE, + "icon_default_bg": "#5C6BC0", + "title_color": ft.Colors.BLUE_GREY_900, + "subtitle_color": ft.Colors.BLUE_GREY_500, + "amount_color": "#2F3C7E", + "tag_text_color": "#4B4F67", + "tag_bg": "#E7E9FB", + "badge_bg": "#35C46B", + } + self.doc_type_palette = { + DocumentType.INVOICE.value: "#5C6BC0", + DocumentType.ESTIMATE.value: "#7E57C2", + DocumentType.DELIVERY.value: "#26A69A", + DocumentType.RECEIPT.value: "#FF7043", + DocumentType.SALES.value: "#42A5F5", + DocumentType.DRAFT.value: "#90A4AE", + } # ビジネスロジックサービス self.app_service = AppService() @@ -134,6 +186,8 @@ class FlutterStyleDashboard: self.page.window.width = 420 self.page.window.height = 900 self.page.theme_mode = ft.ThemeMode.LIGHT + self.page.on_disconnect = self.dispose + self.page.on_close = self.dispose # Fletのライフサイクルに任せる(SystemExitがasyncioに伝播して警告になりやすい) @@ -192,27 +246,7 @@ class FlutterStyleDashboard: 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 %H:%M'), - '完了', - invoice.notes - )) - - self.conn.commit() - except Exception as e: - logging.error(f"サンプルデータ作成エラー: {e}") + logging.warning("create_sample_dataは使用されていません") def setup_ui(self): """UIセットアップ""" @@ -228,6 +262,13 @@ class FlutterStyleDashboard: # 初期表示 self.update_main_content() + + def dispose(self, e=None): + """リソース解放""" + try: + self.app_service.close() + except Exception as err: + logging.warning(f"クリーンアップ失敗: {err}") def on_tab_change(self, index): """タブ切り替え""" @@ -241,7 +282,10 @@ class FlutterStyleDashboard: logging.info(f"update_main_content: current_tab={self.current_tab}, is_customer_picker_open={self.is_customer_picker_open}") - if self.is_customer_picker_open: + if self.is_new_customer_form_open: + logging.info("新規顧客登録画面を表示") + self.main_content.controls.append(self.create_new_customer_screen()) + elif self.is_customer_picker_open: # 顧客選択画面 logging.info("顧客選択画面を表示") self.main_content.controls.append(self.create_customer_picker_screen()) @@ -253,6 +297,10 @@ class FlutterStyleDashboard: # 伝票詳細/編集画面 logging.info("伝票詳細/編集画面を表示") self.main_content.controls.append(self._build_invoice_detail_screen()) + elif self.current_tab == 2: + # マスタ編集画面 + logging.info("マスタ編集画面を表示") + self.main_content.controls.append(self._build_master_editor_screen()) else: # 不明なタブは一覧に戻す self.current_tab = 0 @@ -309,7 +357,9 @@ class FlutterStyleDashboard: show_back=True, show_edit=not is_locked, on_back=lambda _: self.back_to_list(), - on_edit=lambda _: self.toggle_edit_mode() + on_edit=lambda _: self.on_detail_appbar_action(), + action_icon=ft.Icons.SAVE if getattr(self, 'is_detail_edit_mode', False) else ft.Icons.EDIT, + action_tooltip="保存" if getattr(self, 'is_detail_edit_mode', False) else "編集", ) # 伝票詳細コンテンツ(ズーム対応) @@ -328,6 +378,36 @@ class FlutterStyleDashboard: padding=ft.Padding.all(16) ), ], expand=True) + + def on_detail_appbar_action(self): + """詳細画面の右上アクション(編集/保存)""" + if getattr(self, 'is_detail_edit_mode', False): + save_handler = getattr(self, "_detail_save_handler", None) + if save_handler: + save_handler(None) + return + self.toggle_edit_mode() + + def _build_master_editor_screen(self) -> ft.Column: + """マスタ編集画面を構築""" + if self.master_editor is None: + self.master_editor = UniversalMasterEditor(self.page) + + app_bar = AppBar( + title="マスタ編集", + show_back=True, + show_edit=False, + on_back=lambda _: self.back_to_list(), + ) + + return ft.Column([ + app_bar, + ft.Container( + content=self.master_editor.build(), + expand=True, + padding=ft.Padding.all(16), + ), + ], expand=True) def back_to_list(self): """一覧画面に戻る""" @@ -346,25 +426,57 @@ class FlutterStyleDashboard: def _build_invoice_list(self) -> ft.Column: """伝票リストを構築""" logging.info("_build_invoice_list: 開始") - - # 履歴データ読み込み - slips = self.load_slips() + + date_from, date_to = to_date_range(self.explorer_state.period_key) + slips = self.load_slips( + query=self.explorer_state.query, + date_from=date_from, + date_to=date_to, + sort_by=self.explorer_state.sort_key, + sort_desc=self.explorer_state.sort_desc, + limit=self.explorer_state.limit, + offset=self.explorer_state.offset, + include_offsets=self.explorer_state.include_offsets, + ) + self.show_offsets = self.explorer_state.include_offsets logging.info(f"伝票データ取得: {len(slips)}件") - - if not slips: - logging.info("_build_invoice_list: 伝票データなし、空のリストを返す") - return ft.Column([ - ft.Text("伝票データがありません", size=16, color=ft.Colors.GREY_600), - ft.Container(height=20), - ft.Text("データをインポートしてください", size=14, color=ft.Colors.GREY_500) - ], horizontal_alignment=ft.CrossAxisAlignment.CENTER) - - if not self.show_offsets: - slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))] - logging.info(f"赤伝除外後: {len(slips)}件") + + def on_query_change(e): + self.explorer_state.query = (e.control.value or "").strip() + self.explorer_state.offset = 0 + self.update_main_content() + + def on_period_change(e): + self.explorer_state.period_key = e.control.value or "3m" + self.explorer_state.offset = 0 + self.update_main_content() + + def on_sort_change(e): + self.explorer_state.sort_key = e.control.value or "date" + self.explorer_state.offset = 0 + self.update_main_content() + + def on_sort_direction_toggle(_): + self.explorer_state.sort_desc = not self.explorer_state.sort_desc + self.explorer_state.offset = 0 + self.update_main_content() def on_toggle_offsets(e): - self.show_offsets = bool(e.control.value) + self.explorer_state.include_offsets = bool(e.control.value) + self.explorer_state.offset = 0 + self.show_offsets = self.explorer_state.include_offsets + self.update_main_content() + + def on_prev_page(_): + if self.explorer_state.offset <= 0: + return + self.explorer_state.offset = max(0, self.explorer_state.offset - self.explorer_state.limit) + self.update_main_content() + + def on_next_page(_): + if len(slips) < self.explorer_state.limit: + return + self.explorer_state.offset += self.explorer_state.limit self.update_main_content() def on_verify_chain(_): @@ -375,387 +487,269 @@ class FlutterStyleDashboard: self.update_main_content() except Exception as e: logging.error(f"チェーン検証エラー: {e}") - - # 履歴カードリスト + + explorer_controls = ft.Container( + content=ft.Column( + [ + ft.Row( + [ + ft.TextField( + label="検索", + hint_text="伝票番号 / 顧客名 / 種別 / 備考", + value=self.explorer_state.query, + prefix_icon=ft.Icons.SEARCH, + on_change=on_query_change, + expand=True, + dense=True, + ), + ft.Dropdown( + label="期間", + value=self.explorer_state.period_key, + options=[ + ft.dropdown.Option(k, v) + for k, v in EXPLORER_PERIODS.items() + ], + on_select=on_period_change, + width=140, + dense=True, + ), + ft.Dropdown( + label="ソート", + value=self.explorer_state.sort_key, + options=[ + ft.dropdown.Option(k, v) + for k, v in EXPLORER_SORTS.items() + ], + on_select=on_sort_change, + width=150, + dense=True, + ), + ft.IconButton( + icon=ft.Icons.ARROW_DOWNWARD if self.explorer_state.sort_desc else ft.Icons.ARROW_UPWARD, + tooltip="並び順切替", + on_click=on_sort_direction_toggle, + ), + ], + spacing=8, + ), + ft.Row( + [ + ft.Text("赤伝", size=10, color=ft.Colors.WHITE), + ft.Switch( + value=self.explorer_state.include_offsets, + on_change=on_toggle_offsets, + ), + ft.Container(width=10), + ft.Text("保存後詳細に留まる", size=10, color=ft.Colors.WHITE), + ft.Switch( + value=self.stay_on_detail_after_save, + on_change=lambda e: setattr(self, 'stay_on_detail_after_save', bool(e.control.value)), + ), + ft.Text( + f"表示中: {len(slips)}件 / offset={self.explorer_state.offset}", + size=12, + color=ft.Colors.BLUE_GREY_700, + ), + ft.Container(expand=True), + ft.TextButton("◀ 前", on_click=on_prev_page), + ft.TextButton("次 ▶", on_click=on_next_page), + ft.OutlinedButton("マスタ編集", on_click=self.open_master_editor), + ft.OutlinedButton("チェーン検証", on_click=on_verify_chain), + ], + alignment=ft.MainAxisAlignment.START, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ), + ], + spacing=6, + ), + padding=ft.padding.all(10), + bgcolor=ft.Colors.BLUE_GREY_50, + border_radius=8, + ) + + if not slips: + logging.info("_build_invoice_list: 伝票データなし、空のリストを返す") + return ft.Column( + [ + explorer_controls, + ft.Container(height=20), + ft.Text("伝票データがありません", size=16, color=ft.Colors.GREY_600), + ft.Text("検索条件を緩めるか、データを登録してください", size=14, color=ft.Colors.GREY_500), + ], + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + expand=True, + ) + slip_cards = [] for i, slip in enumerate(slips): logging.info(f"伝票{i}: {type(slip)}") - card = self.create_slip_card(slip) - slip_cards.append(card) - + slip_cards.append(self.create_slip_card(slip)) + logging.info(f"カード作成数: {len(slip_cards)}") - - return ft.Column([ - # 検証結果表示(あれば) - ft.Container( - content=self._build_chain_verify_result(), - margin=ft.Margin.only(bottom=10), - ) if self.chain_verify_result else ft.Container(height=0), - - # 伝票リスト - ft.Column( - controls=slip_cards, - spacing=10, - scroll=ft.ScrollMode.AUTO, - expand=True, - ), - ]) - 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() - - # AppBar(戻るボタンあり) - app_bar = AppBar( - title="顧客選択", - show_back=True, - show_edit=False, - on_back=back - ) - - 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 - # 新規伝票作成中の場合は顧客を設定 - if hasattr(self, 'editing_invoice') and self.editing_invoice: - self.editing_invoice.customer = customer - logging.info(f"伝票に顧客を設定: {customer.formal_name}") - else: - 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), - ft.Container(expand=True), - ft.IconButton( - ft.Icons.PERSON_ADD, - tooltip="新規顧客追加", - icon_color=ft.Colors.WHITE, - on_click=lambda _: self.open_new_customer_form(), - ), - ] - ), - padding=ft.padding.all(15), - bgcolor=ft.Colors.BLUE_GREY, - ) - - content = ft.Column( + content_column = ft.Column( [ - app_bar, # AppBarを追加 - ft.Container(content=search_field, padding=ft.padding.all(15)), - ft.Container(content=list_container, padding=ft.padding.symmetric(horizontal=15), expand=True), + explorer_controls, + ft.Container(height=8), + ft.Container( + content=self._build_chain_verify_result(), + margin=ft.Margin.only(bottom=10), + ) if self.chain_verify_result else ft.Container(height=0), + ft.Column( + controls=slip_cards, + spacing=8, + scroll=ft.ScrollMode.AUTO, + expand=True, + ), ], expand=True, ) - # 初期表示 - render_list(self.customers) - - return ft.Container(content=content, 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() - - def on_verify_chain(e=None): - res = self.app_service.invoice.invoice_repo.verify_chain() - self.chain_verify_result = res - 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=16, weight=ft.FontWeight.BOLD), # 文字を小さく - ft.Container(expand=True), - ft.Row([ - ft.IconButton( - ft.Icons.VERIFIED, - tooltip="チェーン検証", - icon_color=ft.Colors.BLUE_300, - icon_size=16, # アイコンを小さく - on_click=on_verify_chain, - ), - ft.Row( - [ - ft.Text("赤伝", size=10, color=ft.Colors.WHITE), # 文字を小さく - ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets), - ], - spacing=3, # 間隔を狭める - ), - ft.IconButton( - ft.Icons.CLEAR_ALL, - icon_size=16, # アイコンを小さく - ), - ], spacing=3), # 間隔を狭める - ]), - padding=ft.padding.all(8), # パディングを狭める - bgcolor=ft.Colors.BLUE_GREY, - ), - - # 検証結果表示(あれば) - ft.Container( - content=self._build_chain_verify_result(), - margin=ft.Margin.only(bottom=5), # 間隔を狭める - ) if self.chain_verify_result else ft.Container(height=0), - - # 履歴リスト(極限密度表示) - ft.Column( - controls=slip_cards, - spacing=0, # カード間隔を0pxに - scroll=ft.ScrollMode.AUTO, - expand=True, - ), - ]), + bgcolor=self.invoice_card_theme.get("page_bg"), + padding=ft.padding.symmetric(horizontal=12, vertical=10), + content=content_column, expand=True, ) - + def can_create_offset_invoice(self, invoice: Invoice) -> bool: """赤伝発行可能かチェック""" - # LOCK済み伝票であること - if not getattr(invoice, 'final_locked', False): + if not getattr(invoice, "final_locked", False): return False - - # すでに赤伝が存在しないこと + try: - import sqlite3 - with sqlite3.connect('sales.db') as conn: + with sqlite3.connect("sales.db") as conn: cursor = conn.cursor() cursor.execute( - 'SELECT COUNT(*) FROM invoices WHERE offset_target_uuid = ?', - (invoice.uuid,) + "SELECT COUNT(*) FROM invoices WHERE offset_target_uuid = ?", + (invoice.uuid,), ) - offset_count = cursor.fetchone()[0] - return offset_count == 0 + return cursor.fetchone()[0] == 0 except Exception as e: logging.error(f"赤伝存在チェックエラー: {e}") return False - - def create_slip_card(self, slip) -> ft.Card: - """伝票カード作成""" - # サービス層からは Invoice オブジェクトが返る + + def create_slip_card(self, slip) -> ft.Container: + """伝票カード作成(コンパクト表示)""" + theme = self.invoice_card_theme + palette = self.doc_type_palette + 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 %H:%M") - status = "赤伝" if getattr(slip, "is_offset", False) else "完了" - # 最初の商品名を取得(複数ある場合は「他」を付与) - if slip.items and len(slip.items) > 0: - first_item_name = slip.items[0].description - if len(slip.items) > 1: - first_item_name += "(他" + str(len(slip.items) - 1) + ")" - else: - first_item_name = "" + invoice_number = slip.invoice_number + dt = slip.date + final_locked = getattr(slip, "final_locked", False) + items = slip.items or [] + first_item = items[0].description if items else "(明細なし)" + extra_count = max(len(items) - 1, 0) else: - slip_id, slip_type, customer_name, amount, date, status, description, created_at = slip - date = date.strftime("%Y-%m-%d %H:%M") - first_item_name = description or "" - - # タイプに応じたアイコンと色 - 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}) - + slip_id, slip_type, customer_name, amount, dt, status, description, created_at = slip + invoice_number = getattr(slip, "invoice_number", f"ID:{slip_id}") + final_locked = status == "完了" + first_item = description or "(明細なし)" + extra_count = 0 + + icon_bg = palette.get(slip_type, theme["icon_default_bg"]) + display_amount = -abs(amount) if isinstance(slip, Invoice) and getattr(slip, "is_offset", False) else amount + def on_single_tap(_): - """シングルタップ:詳細表示""" if isinstance(slip, Invoice): self.open_invoice_detail(slip) - + def on_double_tap(_): - """ダブルタップ:編集モード切替""" if isinstance(slip, Invoice): self.open_invoice_edit(slip) - + def on_long_press(_): - """長押し:コンテキストメニュー""" self.show_context_menu(slip) - - # 赤伝ボタンの表示条件チェック - show_offset_button = False - if isinstance(slip, Invoice): - show_offset_button = self.can_create_offset_invoice(slip) - - # 長押しメニューで操作するため、ボタンは不要 - - display_amount = amount - if isinstance(slip, Invoice) and getattr(slip, "is_offset", False): - display_amount = -abs(amount) - - return ft.GestureDetector( - content=ft.Card( - content=ft.Container( - content=ft.Column([ - ft.Row([ - ft.Container( - content=ft.Text(config["icon"], size=16), # アイコンを少し大きく - width=28, - height=28, - bgcolor=config["color"], - border_radius=14, - alignment=ft.alignment.Alignment(0, 0), + + show_offset_button = isinstance(slip, Invoice) and self.can_create_offset_invoice(slip) + + first_item_label = first_item[:30] + ("…" if len(first_item) > 30 else "") + if extra_count > 0: + first_item_label = f"{first_item_label} 他{extra_count}" + + left_column = ft.Column( + [ + ft.Row( + [ + ft.Container( + content=ft.Text( + slip_type, + size=10, + weight=ft.FontWeight.BOLD, + color=theme["tag_text_color"], ), - ft.Container( - content=ft.Column([ - ft.Text(slip_type, size=7, weight=ft.FontWeight.BOLD), # タイプ文字をさらに小さく - ft.Text(customer_name, size=15, weight=ft.FontWeight.W_500), # 顧客名を1.5倍に - ft.Row([ - ft.Text(first_item_name, size=12, color=ft.Colors.GREY_600), # 商品名をさらに大きく - ft.Container(expand=True), # スペースを取る - ft.Text(f"¥{display_amount:,.0f}", size=11, weight=ft.FontWeight.BOLD), # 金額を右寄せ - ]), - ], - spacing=0, # 行間を最小化 - tight=True, # 余白を最小化 - ), - expand=True, - ), - ]), - ft.Container(height=0), # 間隔を完全に削除 - ft.Row([ - ft.Text(f"{date} | {status}", size=9, color=ft.Colors.GREY_600), # 日付を大きく - ft.Container(expand=True), # スペースを取る - # 赤伝ボタン(条件付きで表示) - ft.Container( - content=ft.IconButton( - icon=ft.icons.REMOVE_CIRCLE_OUTLINE, - icon_color=ft.Colors.RED_500, - icon_size=16, - tooltip="赤伝発行", - on_click=lambda _: self.create_offset_invoice_dialog(slip), - disabled=not show_offset_button - ) if show_offset_button else ft.Container(width=20), - ), - ]), + padding=ft.padding.symmetric(horizontal=8, vertical=2), + bgcolor=theme["tag_bg"], + border_radius=10, + ), + ft.Text(f"No: {invoice_number}", size=11, color=theme["subtitle_color"]), + ft.Text(dt.strftime("%y/%m/%d %H:%M"), size=10, color=theme["subtitle_color"], expand=True, text_align=ft.TextAlign.END), ], - spacing=0, # カラムの行間を最小化 - tight=True, # 余白を最小化 - ), - padding=ft.padding.all(2), # パディングを最小化 + spacing=8, + vertical_alignment=ft.CrossAxisAlignment.CENTER, ), - elevation=0, + ft.Text(f"{customer_name}", size=12, weight=ft.FontWeight.BOLD, color=theme["title_color"]), + ft.Text(first_item_label, size=10, color=theme["subtitle_color"]), + ], + spacing=2, + expand=True, + ) + + right_controls = [ + ft.Text( + f"¥{display_amount:,.0f}", + size=14, + weight=ft.FontWeight.BOLD, + color=theme["amount_color"], + text_align=ft.TextAlign.END, ), + ] + if show_offset_button: + right_controls.append( + ft.IconButton( + icon=ft.icons.REMOVE_CIRCLE_OUTLINE, + icon_color=ft.Colors.RED_400, + icon_size=16, + tooltip="赤伝発行", + on_click=lambda _: self.create_offset_invoice_dialog(slip), + style=ft.ButtonStyle(padding=0, bgcolor={"": ft.Colors.TRANSPARENT}), + ) + ) + + right_column = ft.Column( + right_controls, + spacing=2, + horizontal_alignment=ft.CrossAxisAlignment.END, + ) + + status_chip = ft.Text("✓ LOCK", size=9, color=theme["tag_text_color"]) if final_locked else ft.Container() + + card_body = ft.Container( + content=ft.Row( + [ + ft.Column([left_column, status_chip], spacing=4, expand=True), + right_column, + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + vertical_alignment=ft.CrossAxisAlignment.CENTER, + ), + padding=ft.padding.symmetric(horizontal=12, vertical=8), + bgcolor=theme["card_bg"], + border_radius=theme["card_radius"], + shadow=[theme["shadow"]], + ) + + return ft.GestureDetector( + content=card_body, on_tap=on_single_tap, on_double_tap=on_double_tap, on_long_press=on_long_press, ) - - def create_offset_invoice_dialog(self, invoice: Invoice): - """赤伝発行確認ダイアログ""" - def close_dialog(_): - self.dialog.open = False - self.update_main_content() - - def confirm_create_offset(_): - # 赤伝を発行 - offset_invoice = self.app_service.invoice.create_offset_invoice( - invoice.uuid, - f"相殺伝票: {invoice.invoice_number}" - ) - if offset_invoice: - logging.info(f"赤伝発行成功: {offset_invoice.invoice_number}") - # 一覧を更新 - self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.update_main_content() - else: - logging.error(f"赤伝発行失敗: {invoice.invoice_number}") - close_dialog(_) - - # 確認ダイアログ - self.dialog = ft.AlertDialog( - modal=True, - title=ft.Text("赤伝発行確認"), - content=ft.Column([ - ft.Text(f"以下の伝票の赤伝を発行します。"), - ft.Container(height=10), - ft.Text(f"伝票番号: {invoice.invoice_number}"), - ft.Text(f"顧客: {invoice.customer.formal_name}"), - ft.Text(f"金額: ¥{invoice.total_amount:,.0f}"), - ft.Container(height=10), - ft.Text("赤伝発行後は取り消せません。よろしいですか?", - color=ft.Colors.RED, weight=ft.FontWeight.BOLD), - ], tight=True), - actions=[ - ft.TextButton("キャンセル", on_click=close_dialog), - ft.ElevatedButton( - "赤伝発行", - on_click=confirm_create_offset, - style=ft.ButtonStyle( - color=ft.Colors.WHITE, - bgcolor=ft.Colors.RED_500 - ) - ), - ], - actions_alignment=ft.MainAxisAlignment.END, - ) - - self.dialog.open = True - self.update_main_content() - + def show_context_menu(self, slip): """コンテキストメニューを表示""" if not isinstance(slip, Invoice): @@ -965,8 +959,7 @@ class FlutterStyleDashboard: # 顧客表示・選択 def select_customer(): """顧客選択画面を開く""" - self.current_tab = 2 # 顧客選択タブ - self.update_main_content() + self.open_customer_picker() # 編集モード時は顧客名入力フィールドを表示 if not is_view_mode and not is_locked: @@ -1104,15 +1097,29 @@ class FlutterStyleDashboard: def save_changes(_): if is_locked: return + save_succeeded = False - # 伝票を更新(現在の明細を保持) - # テーブルから実際の値を取得して更新 self.editing_invoice.notes = notes_field.value self.editing_invoice.document_type = self.selected_document_type - - # TODO: テーブルの明細データを取得して更新 - # 現在は編集された明細データが反映されていない - logging.info(f"更新前明細件数: {len(self.editing_invoice.items)}") + + # UIで更新された明細を保存前に正規化して確定 + normalized_items = normalize_invoice_items(self.editing_invoice.items) + validation = validate_invoice_items(normalized_items) + if not validation.ok: + logging.warning(f"伝票保存バリデーションエラー: {validation.errors}") + try: + self.page.snack_bar = ft.SnackBar( + content=ft.Text(validation.errors[0]), + bgcolor=ft.Colors.RED_600, + ) + self.page.snack_bar.open = True + self.page.update() + except Exception: + pass + return + + self.editing_invoice.items = normalized_items + logging.info(f"保存対象明細件数: {len(self.editing_invoice.items)}") for i, item in enumerate(self.editing_invoice.items): logging.info(f" 明細{i+1}: {item.description} x{item.quantity} @¥{item.unit_price}") @@ -1157,7 +1164,13 @@ class FlutterStyleDashboard: ) logging.info(f"create_invoice戻り値: {success}") if success: + save_succeeded = True logging.info(f"伝票作成成功: {self.editing_invoice.invoice_number}") + self.page.snack_bar = ft.SnackBar( + content=ft.Text("伝票を保存しました"), + bgcolor=ft.Colors.GREEN_600, + ) + self.page.snack_bar.open = True # 一覧を更新して新規作成画面を閉じる self.invoices = self.app_service.invoice.get_recent_invoices(20) logging.info(f"更新後伝票件数: {len(self.invoices)}") @@ -1166,27 +1179,57 @@ class FlutterStyleDashboard: self.update_main_content() else: logging.error(f"伝票作成失敗: {self.editing_invoice.invoice_number}") + self.page.snack_bar = ft.SnackBar( + content=ft.Text("保存に失敗しました。編集内容を確認してください。"), + bgcolor=ft.Colors.RED_600, + ) + self.page.snack_bar.open = True + self.page.update() else: # 更新 logging.info(f"=== 伝票更新開 ===") success = self.app_service.invoice.update_invoice(self.editing_invoice) if success: + save_succeeded = True logging.info(f"伝票更新成功: {self.editing_invoice.invoice_number}") - # 一覧を更新して編集画面を閉じる + self.page.snack_bar = ft.SnackBar( + content=ft.Text("伝票を更新しました"), + bgcolor=ft.Colors.GREEN_600, + ) + self.page.snack_bar.open = True + # 一覧データは更新 self.invoices = self.app_service.invoice.get_recent_invoices(20) - self.editing_invoice = None - self.current_tab = 0 # 一覧タブに戻る - self.update_main_content() + # 設定により遷移先を変更 + if not self.stay_on_detail_after_save: + self.editing_invoice = None + self.current_tab = 0 # 一覧タブに戻る else: logging.error(f"伝票更新失敗: {self.editing_invoice.invoice_number}") + self.page.snack_bar = ft.SnackBar( + content=ft.Text("更新に失敗しました。編集内容を確認してください。"), + bgcolor=ft.Colors.RED_600, + ) + self.page.snack_bar.open = True + self.page.update() except Exception as e: logging.error(f"伝票保存エラー: {e}") import traceback logging.error(f"詳細エラー: {traceback.format_exc()}") + self.page.snack_bar = ft.SnackBar( + content=ft.Text("保存中にエラーが発生しました"), + bgcolor=ft.Colors.RED_600, + ) + self.page.snack_bar.open = True + self.page.update() + save_succeeded = False - # 編集モード終了(ビューモードに戻る) - self.is_detail_edit_mode = False # ビューモードに戻る - self.update_main_content() + if save_succeeded: + # 編集モード終了(ビューモードに戻る) + self.is_detail_edit_mode = False # ビューモードに戻る + self.update_main_content() + + # AppBar右上の保存アイコンからも同じ保存処理を呼べるようにする + self._detail_save_handler = save_changes def cancel_edit(_): self.is_detail_edit_mode = False @@ -1271,9 +1314,9 @@ class FlutterStyleDashboard: # 合計金額表示 ft.Container( content=ft.Row([ - ft.Text("合計: ", size=14, weight=ft.FontWeight.BOLD), # 左に詰める + ft.Text("合計(税込): ", size=14, weight=ft.FontWeight.BOLD), # 左に詰める ft.Text( - f"¥{sum(item.subtotal for item in self.editing_invoice.items):,}", + f"¥{self.editing_invoice.total_amount:,}", size=16, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_600 @@ -1412,10 +1455,8 @@ class FlutterStyleDashboard: item.unit_price = 0 logging.error(f"Unit price update error: {e}") - # 合計金額を更新するために画面を更新 - # 空行削除ロジック:商品名が無く数量も単価も0なら削除 - self._remove_empty_items() - self.update_main_content() + # 入力途中で画面全体を再描画すると編集値が飛びやすいため、 + # ここではモデル更新のみに留める(再描画は保存/行追加/行削除時に実施)。 def _remove_empty_items(self): """商品名が無く数量も単価も0の明細を削除""" @@ -1440,119 +1481,16 @@ class FlutterStyleDashboard: def _create_view_mode_table(self, items: List[InvoiceItem]) -> ft.Column: """表示モード:フレキシブルな表形式で整然と表示""" - # ヘッダー行 - header_row = ft.Row([ - ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), # 可変幅 - ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅 - ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Container(width=35), # 削除ボタン用スペースを確保 - ]) - - # データ行 - data_rows = [] - for i, item in enumerate(items): - row = ft.Row([ - ft.Text(item.description, size=12, expand=True), # 可変幅 - ft.Text(str(item.quantity), size=12, width=35, text_align=ft.TextAlign.RIGHT), # 固定幅 - ft.Text(f"¥{item.unit_price:,}", size=12, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 - ft.Text(f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 - ft.Container(width=35), # 削除ボタン用スペース - ]) - data_rows.append(row) - - return ft.Column([ - header_row, - ft.Divider(height=1, color=ft.Colors.GREY_400), - ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高さを制限 - ]) + return build_invoice_items_view_table(items) def _create_edit_mode_table(self, items: List[InvoiceItem], is_locked: bool) -> ft.Column: """編集モード:フレキシブルな表形式""" - # 自動空行追加を無効化(ユーザーが明示的に追加する場合のみ) - # TODO: 必要に応じて空行追加ボタンを提供 - - # 空行の自動追加を無効化 - pass - - # ヘッダー行 - header_row = ft.Row([ - ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), # 可変幅 - ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅 - ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 - ft.Container(width=35), # 削除ボタン用スペースを確保 - ]) - - # データ行 - data_rows = [] - for i, item in enumerate(items): - # 商品名フィールド - product_field = ft.TextField( - value=item.description, - text_size=12, - height=28, - width=None, # 幅を可変に - expand=True, # 可変幅 - border=ft.border.all(1, ft.Colors.BLUE_200), - bgcolor=ft.Colors.WHITE, - content_padding=ft.padding.all(5), # 内部余白を最小化 - on_change=lambda e, idx=i: self._update_item_field(idx, 'description', e.control.value), - ) - - # 数量フィールド - quantity_field = ft.TextField( - value=str(item.quantity), - text_size=12, - height=28, - width=35, # 固定幅 - text_align=ft.TextAlign.RIGHT, - border=ft.border.all(1, ft.Colors.BLUE_200), - bgcolor=ft.Colors.WHITE, - content_padding=ft.padding.all(5), # 内部余白を最小化 - on_change=lambda e, idx=i: self._update_item_field(idx, 'quantity', e.control.value), - keyboard_type=ft.KeyboardType.NUMBER, - ) - - # 単価フィールド - unit_price_field = ft.TextField( - value=f"{item.unit_price:,}", - text_size=12, - height=28, - width=70, # 固定幅 - text_align=ft.TextAlign.RIGHT, - border=ft.border.all(1, ft.Colors.BLUE_200), - bgcolor=ft.Colors.WHITE, - content_padding=ft.padding.all(5), # 内部余白を最小化 - on_change=lambda e, idx=i: self._update_item_field(idx, 'unit_price', e.control.value.replace(',', '')), - keyboard_type=ft.KeyboardType.NUMBER, - ) - - # 削除ボタン - delete_button = ft.IconButton( - ft.Icons.DELETE_OUTLINE, - tooltip="行を削除", - icon_color=ft.Colors.RED_600, - disabled=is_locked, - icon_size=16, - on_click=lambda _, idx=i: self._delete_item_row(idx), - ) - - # データ行 - row = ft.Row([ - product_field, - quantity_field, - unit_price_field, - ft.Text(f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 - delete_button, - ]) - data_rows.append(row) - - return ft.Column([ - header_row, - ft.Divider(height=1, color=ft.Colors.GREY_400), - ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高さを制限 - ]) + return build_invoice_items_edit_table( + items=items, + is_locked=is_locked, + on_update_field=self._update_item_field, + on_delete_row=self._delete_item_row, + ) def create_new_customer_screen(self) -> ft.Container: """新規顧客登録画面""" name_field = ft.TextField(label="顧客名(略称)") @@ -1622,6 +1560,13 @@ class FlutterStyleDashboard: logging.info("顧客選択画面へ遷移") self.is_customer_picker_open = True self.update_main_content() + + def open_master_editor(self, e=None): + """マスタ編集画面を開く""" + self.is_customer_picker_open = False + self.is_new_customer_form_open = False + self.current_tab = 2 + self.update_main_content() def on_customer_selected(self, customer: Customer): """顧客選択時の処理""" @@ -1712,9 +1657,28 @@ class FlutterStyleDashboard: else: logging.error("伝票作成失敗") - def load_slips(self) -> List[Invoice]: - """伝票データ読み込み - サービス層経由""" - return self.app_service.invoice.get_recent_invoices(20) + def load_slips( + self, + query: str = "", + date_from: Optional[str] = None, + date_to: Optional[str] = None, + sort_by: str = "date", + sort_desc: bool = True, + limit: int = 50, + offset: int = 0, + include_offsets: bool = False, + ) -> List[Invoice]: + """伝票データ読み込み - Explorer条件を適用。""" + return self.app_service.invoice.search_invoices( + query=query, + date_from=date_from, + date_to=date_to, + sort_by=sort_by, + sort_desc=sort_desc, + limit=limit, + offset=offset, + include_offsets=include_offsets, + ) def main(page: ft.Page): """メイン関数""" diff --git a/main_simple.py b/main_simple.py deleted file mode 100644 index f857a1b..0000000 --- a/main_simple.py +++ /dev/null @@ -1,311 +0,0 @@ -import flet as ft -import sqlite3 -import datetime -from typing import List, Dict, Optional - -async def main(page: ft.Page): - # データベース初期化 - init_database() - - page.title = "販売アシスト1号" - page.theme_mode = ft.ThemeMode.LIGHT - page.window_width = 450 - page.window_height = 800 - - async def refresh_all(): - await list_view.refresh() - - # Viewの生成 - input_view = get_input_view(page, refresh_all) - list_view = get_list_view(page, input_view) - - # ナビゲーション制御 - async def nav_change(e): - idx = e.control.selected_index - input_view.visible = (idx == 0) - list_view.visible = (idx == 1) - - if list_view.visible: - await list_view.refresh() - - page.update() - - page.navigation_bar = ft.NavigationBar( - selected_index=1, # 最初はリスト - destinations=[ - ft.NavigationBarDestination(icon=ft.Icons.EDIT_NOTE, label="売上起票"), - ft.NavigationBarDestination(icon=ft.Icons.LIST_ALT, label="一覧"), - ], - on_change=nav_change - ) - - # 画面への追加 - page.add(ft.Container( - content=ft.Column([input_view, list_view], expand=True), - padding=10, expand=True - )) - - # 初期表示設定 - input_view.visible = False - list_view.visible = True - await list_view.refresh() - page.update() - -def init_database(): - """Initialize SQLite database""" - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - # Create sales table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, - customer TEXT NOT NULL, - product TEXT NOT NULL, - amount REAL NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - conn.commit() - conn.close() - -class InputView(ft.Column): - def __init__(self, page: ft.Page, on_success): - super().__init__(expand=True, spacing=15, visible=False) - self.my_page = page - self.on_success = on_success - self.edit_id = None - - # UI部品の定義 - self.title = ft.Text("売上伝票の起票", size=24, weight="bold", color=ft.Colors.BLUE_900) - - self.date_tf = ft.TextField( - label="伝票日付", - value=datetime.datetime.now().strftime("%Y-%m-%d"), - width=200, - border_radius=10 - ) - - self.customer_tf = ft.TextField(label="顧客名", border_radius=10) - self.product_tf = ft.TextField(label="項目/商品名", border_radius=10) - - self.amount_tf = ft.TextField( - label="金額 (税込)", - width=200, - keyboard_type=ft.KeyboardType.NUMBER, - prefix_icon=ft.Icons.ATTACH_MONEY, - border_radius=10 - ) - - self.save_btn = ft.ElevatedButton( - "保存", - icon=ft.Icons.SAVE, - on_click=self.save_data, - bgcolor=ft.Colors.BLUE_900, - color=ft.Colors.WHITE - ) - - self.controls = [ - self.title, - ft.Row([self.date_tf, self.customer_tf], spacing=10), - self.product_tf, - self.amount_tf, - self.save_btn - ] - - async def save_data(self, e): - try: - if not all([self.customer_tf.value, self.product_tf.value, self.amount_tf.value]): - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text("すべての項目を入力してください"), - bgcolor=ft.Colors.RED_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - return - - amount = float(self.amount_tf.value) - - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - - if self.edit_id: - cursor.execute(''' - UPDATE sales - SET date=?, customer=?, product=?, amount=? - WHERE id=? - ''', (self.date_tf.value, self.customer_tf.value, self.product_tf.value, amount, self.edit_id)) - else: - cursor.execute(''' - INSERT INTO sales (date, customer, product, amount) - VALUES (?, ?, ?, ?) - ''', (self.date_tf.value, self.customer_tf.value, self.product_tf.value, amount)) - - conn.commit() - conn.close() - - # フォームをクリア - self.clear_form() - - # 成功メッセージ - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text("保存しました"), - bgcolor=ft.Colors.GREEN_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - - # 一覧を更新 - await self.on_success() - - except ValueError: - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text("金額は数値で入力してください"), - bgcolor=ft.Colors.RED_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - except Exception as ex: - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text(f"エラー: {str(ex)}"), - bgcolor=ft.Colors.RED_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - - def clear_form(self): - self.customer_tf.value = "" - self.product_tf.value = "" - self.amount_tf.value = "" - self.date_tf.value = datetime.datetime.now().strftime("%Y-%m-%d") - self.edit_id = None - self.update() - -class ListView(ft.Column): - def __init__(self, page: ft.Page, input_view): - super().__init__(expand=True, visible=True) - self.my_page = page - self.input_view = input_view - self.data_list = ft.ListView(expand=True, spacing=5) - - self.title = ft.Text("売上一覧", size=20, weight="bold") - self.controls = [self.title, self.data_list] - - async def refresh(self): - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - cursor.execute(''' - SELECT id, date, customer, product, amount - FROM sales - ORDER BY date DESC, created_at DESC - LIMIT 50 - ''') - - self.data_list.controls.clear() - - for row in cursor.fetchall(): - sale_id, date, customer, product, amount = row - - card = ft.Card( - content=ft.Container( - content=ft.ListTile( - title=ft.Text(f"{customer} - {product}"), - subtitle=ft.Text(f"{date} | ¥{amount:,.0f}"), - trailing=ft.Row([ - ft.IconButton( - icon=ft.Icons.EDIT, - on_click=lambda e, id=sale_id: self.edit_data(id) - ), - ft.IconButton( - icon=ft.Icons.DELETE, - on_click=lambda e, id=sale_id: self.delete_data(id) - ) - ]) - ), - padding=10 - ) - ) - self.data_list.controls.append(card) - - conn.close() - self.update() - - except Exception as ex: - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text(f"データ読み込みエラー: {str(ex)}"), - bgcolor=ft.Colors.RED_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - - def edit_data(self, sale_id): - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - cursor.execute(''' - SELECT date, customer, product, amount - FROM sales - WHERE id=? - ''', (sale_id,)) - - row = cursor.fetchone() - conn.close() - - if row: - date, customer, product, amount = row - self.input_view.date_tf.value = date - self.input_view.customer_tf.value = customer - self.input_view.product_tf.value = product - self.input_view.amount_tf.value = str(amount) - self.input_view.edit_id = sale_id - - # 入力画面を表示 - self.input_view.visible = True - self.visible = False - self.my_page.update() - - except Exception as ex: - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text(f"データ読み込みエラー: {str(ex)}"), - bgcolor=ft.Colors.RED_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - - def delete_data(self, sale_id): - try: - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - cursor.execute("DELETE FROM sales WHERE id=?", (sale_id,)) - conn.commit() - conn.close() - - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text("削除しました"), - bgcolor=ft.Colors.GREEN_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - - # 一覧を更新 - self.refresh() - - except Exception as ex: - self.my_page.snack_bar = ft.SnackBar( - content=ft.Text(f"削除エラー: {str(ex)}"), - bgcolor=ft.Colors.RED_500 - ) - self.my_page.snack_bar.open = True - self.my_page.update() - -def get_input_view(page: ft.Page, on_success): - return InputView(page, on_success) - -def get_list_view(page: ft.Page, input_view): - return ListView(page, input_view) - -if __name__ == "__main__": - ft.app(target=main) diff --git a/minimal.py b/minimal.py deleted file mode 100644 index 5070bd9..0000000 --- a/minimal.py +++ /dev/null @@ -1,177 +0,0 @@ -import flet as ft -import sqlite3 -import signal -import sys -import logging -from datetime import datetime - -def init_db(): - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sales ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - customer TEXT NOT NULL, - product TEXT NOT NULL, - amount REAL NOT NULL, - date TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - conn.commit() - conn.close() - return True - -def load_sales(): - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - cursor.execute(''' - SELECT customer, product, amount, date - FROM sales - ORDER BY created_at DESC - LIMIT 20 - ''') - sales = cursor.fetchall() - conn.close() - return sales - -def add_sale(customer, product, amount): - conn = sqlite3.connect('sales.db') - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO sales (customer, product, amount, date) - VALUES (?, ?, ?, ?) - ''', (customer, product, float(amount), datetime.now().strftime("%Y-%m-%d"))) - conn.commit() - conn.close() - return True - -def cleanup_resources(): - """リソースをクリーンアップ""" - try: - logging.info("アプリケーション終了処理開始") - print("✅ 正常終了処理完了") - logging.info("アプリケーション正常終了") - except Exception as e: - logging.error(f"クリーンアップエラー: {e}") - print(f"❌ クリーンアップエラー: {e}") - -def signal_handler(signum, frame): - """シグナルハンドラ""" - print(f"\nシグナル {signum} を受信しました") - cleanup_resources() - sys.exit(0) - -def main(page: ft.Page): - # ログ設定 - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('app.log'), - logging.StreamHandler() - ] - ) - - # シグナルハンドラ設定 - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - logging.info("アプリケーション起動") - - # データベース初期化 - try: - init_db() - logging.info("データベース初期化完了") - except Exception as e: - logging.error(f"データベース初期化エラー: {e}") - print(f"❌ データベース初期化エラー: {e}") - - page.title = "販売アシスト" - page.window_width = 400 - page.window_height = 600 - - # ウィンドウクローズイベント - def on_window_close(e): - logging.info("ウィンドウクローズイベント") - cleanup_resources() - - page.on_window_close = on_window_close - - # UI要素定義 - customer = ft.TextField(label="顧客名") - product = ft.TextField(label="商品名") - amount = ft.TextField(label="金額") - list_view = ft.Column() - - def add_sale_clicked(e): - if customer.value and product.value and amount.value: - try: - # 保存前に値を取得 - customer_val = customer.value - product_val = product.value - amount_val = amount.value - - # データベースに保存 - add_sale(customer_val, product_val, amount_val) - - # フィールドをクリア - customer.value = "" - product.value = "" - amount.value = "" - - # リスト更新 - update_list() - - # 成功メッセージ - page.snack_bar = ft.SnackBar( - content=ft.Text("保存しました"), - bgcolor=ft.Colors.GREEN - ) - page.snack_bar.open = True - page.update() - - logging.info(f"売上データ追加: {customer_val} {product_val} {amount_val}") - except Exception as ex: - logging.error(f"保存エラー: {ex}") - page.snack_bar = ft.SnackBar( - content=ft.Text("エラーが発生しました"), - bgcolor=ft.Colors.RED - ) - page.snack_bar.open = True - page.update() - - def update_list(): - try: - list_view.controls.clear() - sales = load_sales() - for sale in sales: - customer, product, amount, date = sale - list_view.controls.append( - ft.Text(f"{date}: {customer} - {product}: ¥{amount:,.0f}") - ) - except Exception as e: - logging.error(f"リスト更新エラー: {e}") - - # ボタン定義(関数定義後) - add_btn = ft.Button("追加", on_click=add_sale_clicked) - - # 初期データ読み込み - update_list() - - logging.info("UI初期化完了") - print("🚀 アプリケーション起動完了") - - page.add( - ft.Text("売上管理", size=20), - customer, - product, - amount, - add_btn, - ft.Divider(), - ft.Text("売上一覧", size=16), - list_view - ) - -if __name__ == "__main__": - ft.app(target=main) diff --git a/run_4k.py b/run_4k.py deleted file mode 100755 index a69fd8b..0000000 --- a/run_4k.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -"""4K環境対応Fletアプリ起動スクリプト""" - -import os -import subprocess -import sys - -def main(): - # 4K環境向けスケーリング設定 - env = os.environ.copy() - - # Linux Mint + Compiz + 4K向け設定 - env.update({ - 'FLET_FORCE_PIXEL_RATIO': '2.0', # 4K用に2倍スケーリング - 'GDK_SCALE': '2', # GTKスケーリング - 'GDK_DPI_SCALE': '2', # DPIスケーリング - 'QT_SCALE_FACTOR': '2', # Qtスケーリング - 'QT_AUTO_SCREEN_SCALE_FACTOR': '2', # Qt自動スケーリング - 'DISPLAY': ':0', - 'XDG_SESSION_TYPE': 'x11', # X11強制 - 'GDK_BACKEND': 'x11' # X11バックエンド強制 - }) - - print("🚀 4K環境で販売アシスト1号を起動します...") - print(f"スケーリング設定: {dict((k, v) for k, v in env.items() if 'SCALE' in k or 'DPI' in k or 'RATIO' in k)}") - - try: - # Fletアプリを起動 - subprocess.run([ - sys.executable, 'main_simple.py' - ], env=env, check=True) - print("✅ アプリが正常に終了しました") - - except subprocess.CalledProcessError as e: - print(f"❌ 起動エラー: {e}") - return 1 - except KeyboardInterrupt: - print("\n👋 アプリを終了します") - return 0 - except Exception as e: - print(f"❌ 予期せぬエラー: {e}") - return 1 - - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/run_linux.py b/run_linux.py deleted file mode 100755 index cc786d9..0000000 --- a/run_linux.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -"""Linux Mint向けFletアプリ起動スクリプト""" - -import os -import subprocess -import sys - -def main(): - # Linux Mint向け環境変数設定 - env = os.environ.copy() - env.update({ - 'FLET_FORCE_PIXEL_RATIO': '1.0', - 'GDK_SCALE': '1', - 'GDK_BACKEND': 'x11', - 'QT_SCALE_FACTOR': '1', - 'DISPLAY': ':0' - }) - - print("🚀 販売アシスト1号を起動します...") - print(f"環境変数: {dict(env)}") - - try: - # Fletアプリを起動 - subprocess.run([ - sys.executable, 'main_simple.py' - ], env=env, check=True) - print("✅ アプリが正常に終了しました") - - except subprocess.CalledProcessError as e: - print(f"❌ 起動エラー: {e}") - return 1 - except KeyboardInterrupt: - print("\n👋 アプリを終了します") - return 0 - except Exception as e: - print(f"❌ 予期せぬエラー: {e}") - return 1 - - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/services/app_service.py b/services/app_service.py index 549527a..c19b5fe 100644 --- a/services/app_service.py +++ b/services/app_service.py @@ -274,6 +274,33 @@ class InvoiceService: """最近の伝票を取得""" return self.invoice_repo.get_all_invoices(limit) + def search_invoices( + self, + query: str = "", + date_from: Optional[str] = None, + date_to: Optional[str] = None, + sort_by: str = "date", + sort_desc: bool = True, + limit: int = 50, + offset: int = 0, + include_offsets: bool = False, + ) -> List[Invoice]: + """Explorer向けに条件検索で伝票を取得。""" + return self.invoice_repo.search_invoices( + query=query, + date_from=date_from, + date_to=date_to, + sort_by=sort_by, + sort_desc=sort_desc, + limit=limit, + offset=offset, + include_offsets=include_offsets, + ) + + def verify_chain(self, node_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]: + """監査用チェーン整合性を検証。""" + return self.invoice_repo.verify_chain(node_id=node_id, limit=limit) + def regenerate_pdf(self, invoice_uuid: str) -> Optional[str]: """DBを正としてPDFを再生成(生成物は仮)""" invoice = self.invoice_repo.get_invoice_by_uuid(invoice_uuid) @@ -343,6 +370,11 @@ class InvoiceService: logging.error(f"自社情報更新エラー: {e}") return False + def close(self): + """リソース解放(将来の接続保持に備える)""" + # Repositoryは都度接続を開閉するため処理なし + logging.debug("InvoiceService.close called") + class CustomerService: """顧客ビジネスロジック""" @@ -414,6 +446,10 @@ class CustomerService: self._customer_cache = [c for c in self._customer_cache if c.id != customer_id] return True + def close(self): + """キャッシュや将来の接続用クローズ""" + logging.debug("CustomerService.close called") + # サービスファクトリ class ProductService: @@ -429,6 +465,9 @@ class ProductService: def save_product(self, product: Product) -> bool: """商品を保存(新規・更新)""" return self.invoice_repo.save_product(product) + + def close(self): + logging.debug("ProductService.close called") class AppService: @@ -452,6 +491,21 @@ class AppService: 'customer_count': len(self.customer.get_all_customers()) } + def close(self): + """サービス全体のリソース解放""" + try: + self.invoice.close() + except Exception as err: + logging.warning(f"InvoiceService close failed: {err}") + try: + self.customer.close() + except Exception as err: + logging.warning(f"CustomerService close failed: {err}") + try: + self.product.close() + except Exception as err: + logging.warning(f"ProductService close failed: {err}") + # 使用例 if __name__ == "__main__": diff --git a/services/repositories.py b/services/repositories.py index 511aab9..8ab4ef0 100644 --- a/services/repositories.py +++ b/services/repositories.py @@ -139,6 +139,13 @@ class InvoiceRepository: ensure_column("invoices", "pdf_sha256", "pdf_sha256 TEXT") ensure_column("invoices", "submitted_to_tax_authority", "submitted_to_tax_authority INTEGER DEFAULT 0") + # Explorer向けインデックス(大量データ閲覧時の検索性能を確保) + cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_date ON invoices(date)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_customer_name ON invoices(customer_name)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_document_type ON invoices(document_type)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_updated_at ON invoices(updated_at)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_is_offset ON invoices(is_offset)") + conn.commit() def get_node_id(self) -> str: @@ -419,28 +426,78 @@ class InvoiceRepository: def get_all_invoices(self, limit: int = 100) -> List[Invoice]: """全伝票を取得""" - invoices = [] - + return self.search_invoices(limit=limit) + + def search_invoices( + self, + query: str = "", + date_from: Optional[str] = None, + date_to: Optional[str] = None, + sort_by: str = "date", + sort_desc: bool = True, + limit: int = 50, + offset: int = 0, + include_offsets: bool = False, + ) -> List[Invoice]: + """Explorer向けに条件検索で伝票を取得。""" + invoices: List[Invoice] = [] + orderable_columns = { + "date": "date", + "invoice_number": "invoice_number", + "customer_name": "customer_name", + "document_type": "document_type", + "updated_at": "updated_at", + } + order_column = orderable_columns.get(sort_by, "date") + order_direction = "DESC" if sort_desc else "ASC" + + where_clauses = ["1=1"] + params: List[Any] = [] + + if not include_offsets: + where_clauses.append("(is_offset IS NULL OR is_offset = 0)") + + q = (query or "").strip() + if q: + like = f"%{q}%" + where_clauses.append( + "(" + "invoice_number LIKE ? OR " + "customer_name LIKE ? OR " + "document_type LIKE ? OR " + "notes LIKE ?" + ")" + ) + params.extend([like, like, like, like]) + + if date_from: + where_clauses.append("date >= ?") + params.append(date_from) + if date_to: + where_clauses.append("date <= ?") + params.append(date_to) + + sql = f""" + SELECT * FROM invoices + WHERE {' AND '.join(where_clauses)} + ORDER BY {order_column} {order_direction}, id DESC + LIMIT ? OFFSET ? + """ + params.extend([int(limit), int(offset)]) + try: with sqlite3.connect(self.db_path) as conn: cursor = conn.cursor() - - cursor.execute(''' - SELECT * FROM invoices - ORDER BY date DESC - LIMIT ? - ''', (limit,)) - + cursor.execute(sql, tuple(params)) 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}") - + logging.error(f"伝票検索エラー: {e}") + return invoices def get_invoice_by_uuid(self, invoice_uuid: str) -> Optional[Invoice]: diff --git a/test_force_size.py b/test_force_size.py deleted file mode 100644 index 874bc91..0000000 --- a/test_force_size.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -強制ウィンドウサイズテスト -""" - -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 deleted file mode 100644 index 2a894fa..0000000 --- a/test_size_1.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -ウインドウサイズテスト - 手法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 deleted file mode 100644 index fc30ce6..0000000 --- a/test_size_2.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -ウインドウサイズテスト - 手法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 deleted file mode 100644 index 693cb8c..0000000 --- a/test_size_3.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -ウインドウサイズテスト - 手法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 deleted file mode 100644 index 95d8d89..0000000 --- a/test_size_4.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -ウインドウサイズテスト - 手法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 deleted file mode 100644 index 9ec917f..0000000 --- a/test_size_5.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -ウインドウサイズテスト - 手法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)