一覧で苦しむ

This commit is contained in:
joe 2026-02-23 08:57:59 +09:00
parent 1344f1d90b
commit cbd800c13d
180 changed files with 1170 additions and 21204 deletions

198
README.md
View file

@ -1,113 +1,149 @@
# 販売アシスト1号 # 販売アシスト1号
Python + Fletで開発したAndroid対応販売管理アプリケーションです。 Python + Flet で開発した、Android向けスタンドアロン販売管理アプリです。
## 機能 ## 主な機能
- **ダッシュボード**: 顧客数、商品数、売上件数、総売上を表示 - ダッシュボード(顧客数・商品数・売上件数・総売上)
- **顧客管理**: 顧客情報の追加、編集、削除 - 顧客管理(追加・編集・削除)
- **商品管理**: 商品情報の追加、編集、削除、在庫管理 - 商品管理(追加・編集・削除・在庫管理)
- **売上管理**: 売上データの記録と閲覧 - 売上管理(記録・閲覧)
- **データ出力**: JSON/CSV形式でのデータエクスポート - データ出力JSON/CSV
- **電子帳簿保存法対応**: 10年間データ保持、監査証跡、整合性チェック
## 電子帳簿保存法対応
- **10年間データ保持**: 法定期間のデータ保存に対応
- **監査証跡**: 全データ操作のログ記録
- **データ整合性**: チェックサムによる改ざん検知
- **アーカイブ機能**: 7年以上前のデータを自動アーカイブ
- **コンプライアンスレポート**: 法令対応状況の定期報告
## セットアップ ## セットアップ
1. 依存関係をインストール:
```bash ```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt pip install -r requirements.txt
``` ```
2. アプリケーションを実行: ## 実行
```bash ```bash
python main.py 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ビルド ## Androidビルド
Fletを使用してAndroidアプリをビルド: ### 直接コマンド
```bash ```bash
flet build apk . flet build apk . --module-name main
``` ```
リリースAABを作る場合: リリース用AAB:
```bash ```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 <APKのパス>
```
## トラブルシュート
### 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
bash scripts/auto_recover_and_build.sh /home/user/dev/h-1.flet.3 bash scripts/auto_recover_and_build.sh /home/user/dev/h-1.flet.3
``` ```
このスクリプトで実行される内容: 実行内容:
- プロジェクト全体のバックアップ作成 - プロジェクトバックアップ作成
- 生成物/試作ファイルの `trash/<timestamp>/` への移動 - 生成物の `trash/<timestamp>/` 移動
- `.gitignore` の整備 - `.gitignore` 整備
- Gitベースラインコミット作成必要時 - 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年間の長期保存
- 検索・閲覧の容易性
- 定期的な整合性検証

View file

@ -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:
# NavigationBarCompiz対応
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)

View file

@ -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:
# NavigationBarCompiz対応
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)

View file

@ -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)

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -1,40 +1,48 @@
"""Androidビルド用スクリプト""" """Androidビルド用スクリプト"""
import subprocess import subprocess
import sys import sys
import os
def build_android():
"""販売アシスト1号をAndroidアプリとしてビルド""" def build_android(target: str = "apk") -> bool:
"""販売アシスト1号をAndroidアプリとしてビルド。targetはapk/aab。"""
print("🚀 販売アシスト1号 Androidビルド開始...") if target not in {"apk", "aab"}:
print(f"❌ 未対応ターゲット: {target}apk / aab を指定してください)")
# FletでAndroidビルド return False
print(f"🚀 販売アシスト1号 Androidビルド開始... target={target}")
try: try:
result = subprocess.run([ result = subprocess.run(
sys.executable, "-m", "flet", "pack", "main.py", ["flet", "build", target, ".", "--module-name", "main", "--yes"],
"--android", check=True,
"--name", "販売アシスト1号", capture_output=True,
"--package-name", "com.sales.assistant1", text=True,
"--icon", "icon.png" # アイコンがあれば )
], check=True, capture_output=True, text=True)
print("✅ ビルド成功!") 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: except subprocess.CalledProcessError as e:
print("❌ ビルド失敗:") print("❌ ビルド失敗:")
print(e.stderr) if e.stdout:
print(e.stdout)
if e.stderr:
print(e.stderr)
return False return False
except FileNotFoundError:
print("❌ Fletがインストールされていません")
print("pip install flet を実行してください")
return False
return True
if __name__ == "__main__": if __name__ == "__main__":
if build_android(): target = "apk"
print("\n🎉 販売アシスト1号のAndroidビルドが完了しました!") if len(sys.argv) > 1:
print("生成されたAPKファイルをAndroid端末にインストールしてください") target = sys.argv[1].strip().lower()
if build_android(target):
print(f"\n🎉 Androidビルド完了: {target}")
print("生成物は build/ または dist/ 配下を確認してください")
else: else:
print("\n💥 ビルドに失敗しました") print("\n💥 ビルドに失敗しました")

View file

@ -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),
]
)

View file

@ -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()

View file

@ -193,7 +193,7 @@ class SlipEntryFramework:
self.theme_dropdown = ft.Dropdown( self.theme_dropdown = ft.Dropdown(
label="表示形式", label="表示形式",
options=[], options=[],
on_change=self.change_theme on_select=self.change_theme
) )
# 入力フォーム # 入力フォーム

View file

@ -9,6 +9,8 @@ import logging
from datetime import datetime from datetime import datetime
from typing import List, Dict, Optional, Callable from typing import List, Dict, Optional, Callable
from components.explorer_framework import ExplorerQueryState
class UniversalMasterEditor: class UniversalMasterEditor:
"""汎用マスタ編集コンポーネント""" """汎用マスタ編集コンポーネント"""
@ -32,9 +34,8 @@ class UniversalMasterEditor:
'table_name': 'products', 'table_name': 'products',
'fields': [ 'fields': [
{'name': 'name', 'label': '商品名', 'width': 200, 'required': True}, {'name': 'name', 'label': '商品名', 'width': 200, 'required': True},
{'name': 'category', 'label': 'カテゴリ', 'width': 150}, {'name': 'unit_price', 'label': '単価', 'width': 120, 'keyboard_type': ft.KeyboardType.NUMBER, 'required': True},
{'name': 'price', 'label': '価格', 'width': 100, 'keyboard_type': ft.KeyboardType.NUMBER, 'required': True}, {'name': 'description', 'label': '説明', 'width': 320}
{'name': 'stock', 'label': '在庫数', 'width': 100, 'keyboard_type': ft.KeyboardType.NUMBER}
] ]
}, },
'sales_slips': { 'sales_slips': {
@ -53,6 +54,14 @@ class UniversalMasterEditor:
self.current_master = 'customers' self.current_master = 'customers'
self.current_data = [] self.current_data = []
self.editing_id = None 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部品 # UI部品
self.title = ft.Text("", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) 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") ft.dropdown.Option("伝票マスタ", "sales_slips")
], ],
value="customers", value="customers",
on_change=self.change_master on_select=self.change_master
) )
# フォーム # フォーム
self.form_fields = ft.Column([], spacing=10) 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) 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._build_form()
self._refresh_sort_options()
self._load_data() self._load_data()
def change_master(self, e): def change_master(self, e):
"""マスタを切り替え""" """マスタを切り替え"""
self.current_master = e.control.value self.current_master = e.control.value
self.editing_id = None 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.title.value = self.masters[self.current_master]['title']
self.search_field.value = ""
self._build_form() 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._load_data()
self.page.update() self.page.update()
@ -121,19 +216,43 @@ class UniversalMasterEditor:
cursor = conn.cursor() cursor = conn.cursor()
table_name = self.masters[self.current_master]['table_name'] 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} 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() self.current_data = cursor.fetchall()
col_names = [col[0] for col in cursor.description]
conn.close() conn.close()
# データリストを更新 # データリストを更新
self.data_list.controls.clear() self.data_list.controls.clear()
for item in self.current_data: for item in self.current_data:
item_id = item[0] 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 = [] row_controls = []
@ -173,6 +292,10 @@ class UniversalMasterEditor:
) )
self.data_list.controls.append(data_card) 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() self.page.update()
@ -186,10 +309,22 @@ class UniversalMasterEditor:
form_data = {} form_data = {}
for i, field in enumerate(self.masters[self.current_master]['fields']): for i, field in enumerate(self.masters[self.current_master]['fields']):
field_control = self.form_fields.controls[i] 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) self._show_snackbar(f"{field['label']}は必須項目です", ft.Colors.RED)
return return
@ -339,6 +474,24 @@ class UniversalMasterEditor:
ft.Divider(), ft.Divider(),
ft.Text(f"{self.masters[self.current_master]['title']}一覧", size=18, weight=ft.FontWeight.BOLD), ft.Text(f"{self.masters[self.current_master]['title']}一覧", size=18, weight=ft.FontWeight.BOLD),
self.instructions, 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 self.data_list
], expand=True, spacing=15) ], expand=True, spacing=15)

View file

@ -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)

View file

@ -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

View file

@ -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'

View file

@ -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.

View file

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

View file

@ -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

View file

@ -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

View file

@ -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 = "../.."
}

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

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

View file

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

View file

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

View file

@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

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

View file

@ -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")

View file

@ -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()

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
}
}

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

@ -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.
}
}

View file

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

View file

@ -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<MainNavigationShell> createState() => _MainNavigationShellState();
}
class _MainNavigationShellState extends State<MainNavigationShell> {
int _selectedIndex = 0;
//
final List<Widget> _screens = [];
@override
void initState() {
super.initState();
_screens.addAll([
InvoiceFlowScreen(onMoveToHistory: () => _onItemTapped(1)),
const InvoiceHistoryScreen(),
]);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
//
void _openCompanyEditor(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompanyEditorScreen(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _selectedIndex,
children: _screens,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.add_box),
label: '新規作成',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: '発行履歴',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.indigo,
onTap: _onItemTapped,
),
);
}
}
///
class InvoiceFlowScreen extends StatelessWidget {
final VoidCallback onMoveToHistory;
const InvoiceFlowScreen({super.key, required this.onMoveToHistory});
// PDF
void _handleInvoiceGenerated(BuildContext context, Invoice generatedInvoice, String filePath) {
// PDF生成DB保存後に詳細ページへ遷移
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(invoice: generatedInvoice),
),
);
}
//
void _openCompanyEditor(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompanyEditorScreen(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
//
title: GestureDetector(
onLongPress: () => _openCompanyEditor(context),
child: const Text("販売アシスト1号 V1.4.3c"),
),
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
),
//
body: InvoiceInputForm(
onInvoiceGenerated: (invoice, path) => _handleInvoiceGenerated(context, invoice, path),
),
);
}
}

View file

@ -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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

View file

@ -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<String, dynamic> toJson() {
return {
'id': id,
'formal_name': formalName,
'representative': representative,
'zip_code': zipCode,
'address': address,
'tel': tel,
'fax': fax,
'email': email,
'website': website,
'registration_number': registrationNumber,
'notes': notes,
};
}
/// JSONからモデルを生成
factory Company.fromJson(Map<String, dynamic> json) {
return Company(
id: json['id'] as String,
formalName: json['formal_name'] as String,
representative: json['representative'] as String?,
zipCode: json['zip_code'] as String?,
address: json['address'] as String?,
tel: json['tel'] as String?,
fax: json['fax'] as String?,
email: json['email'] as String?,
website: json['website'] as String?,
registrationNumber: json['registration_number'] as String?,
notes: json['notes'] as String?,
);
}
// ()
static const Company defaultCompany = Company(
id: 'my_company',
formalName: '自社名が入ります',
zipCode: '〒000-0000',
address: '住所がここに入ります',
tel: 'TEL: 00-0000-0000',
registrationNumber: '適格請求書発行事業者登録番号 T1234567890123', //
notes: 'いつもお世話になっております。',
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<File> _getCustomerFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_customerFileName');
}
///
Future<File> _getProductFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_productFileName');
}
///
Future<File> _getCompanyFile() async {
final directory = await getApplicationDocumentsDirectory();
return File('${directory.path}/$_companyFileName');
}
// --- ---
///
Future<List<Customer>> loadCustomers() async {
try {
final file = await _getCustomerFile();
if (!await file.exists()) return [];
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((j) => Customer.fromJson(j)).toList();
} catch (e) {
debugPrint('Customer Master Loading Error: $e');
return [];
}
}
///
Future<void> saveCustomers(List<Customer> customers) async {
try {
final file = await _getCustomerFile();
final String encoded = json.encode(customers.map((c) => c.toJson()).toList());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Customer Master Saving Error: $e');
}
}
///
Future<void> upsertCustomer(Customer customer) async {
final customers = await loadCustomers();
final index = customers.indexWhere((c) => c.id == customer.id);
if (index != -1) {
customers[index] = customer;
} else {
customers.add(customer);
}
await saveCustomers(customers);
}
// --- ---
///
/// ProductMasterに定義された初期データを返す
Future<List<Product>> loadProducts() async {
try {
final file = await _getProductFile();
if (!await file.exists()) {
// ProductMasterのハードコードされたリストを返す
return List.from(ProductMaster.products);
}
final String content = await file.readAsString();
final List<dynamic> jsonList = json.decode(content);
return jsonList.map((j) => Product.fromJson(j)).toList();
} catch (e) {
debugPrint('Product Master Loading Error: $e');
return List.from(ProductMaster.products); //
}
}
///
Future<void> saveProducts(List<Product> products) async {
try {
final file = await _getProductFile();
final String encoded = json.encode(products.map((p) => p.toJson()).toList());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Product Master Saving Error: $e');
}
}
///
Future<void> upsertProduct(Product product) async {
final products = await loadProducts();
final index = products.indexWhere((p) => p.id == product.id);
if (index != -1) {
products[index] = product;
} else {
products.add(product);
}
await saveProducts(products);
}
// --- ---
///
/// Company.defaultCompany
Future<Company> loadCompany() async {
try {
final file = await _getCompanyFile();
if (!await file.exists()) {
return Company.defaultCompany;
}
final String content = await file.readAsString();
final Map<String, dynamic> jsonMap = json.decode(content);
return Company.fromJson(jsonMap);
} catch (e) {
debugPrint('Company Info Loading Error: $e');
return Company.defaultCompany; //
}
}
///
Future<void> saveCompany(Company company) async {
try {
final file = await _getCompanyFile();
final String encoded = json.encode(company.toJson());
await file.writeAsString(encoded);
} catch (e) {
debugPrint('Company Info Saving Error: $e');
}
}
}

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