319 lines
No EOL
10 KiB
Dart
319 lines
No EOL
10 KiB
Dart
// DatabaseHelper - シンプルデータベースアクセスヘルパー(sqflite 直接操作)
|
||
// NOTE: データベース更新メソッドは簡素化のため、update() を使用していません
|
||
|
||
import 'dart:io';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:sqflite/sqflite.dart';
|
||
import '../models/product.dart';
|
||
|
||
// Customer モデル
|
||
class Customer {
|
||
final int? id;
|
||
final String? customerCode;
|
||
final String? name;
|
||
final String? address;
|
||
final String? phone;
|
||
final String? email;
|
||
final bool isInactive;
|
||
|
||
Customer({
|
||
this.id,
|
||
this.customerCode,
|
||
this.name,
|
||
this.address,
|
||
this.phone,
|
||
this.email,
|
||
this.isInactive = false,
|
||
});
|
||
}
|
||
|
||
class DatabaseHelper {
|
||
static Database? _database;
|
||
|
||
/// データベース初期化(サンプルデータ付き)
|
||
static Future<void> init() async {
|
||
if (_database != null) return;
|
||
|
||
try {
|
||
String dbPath;
|
||
|
||
if (Platform.isAndroid || Platform.isIOS) {
|
||
// モバイル環境:sqflite の標準パスを使用
|
||
final dbDir = await getDatabasesPath();
|
||
dbPath = '$dbDir/sales.db';
|
||
} else {
|
||
// デスクトップ/開発環境:現在のフォルダを使用
|
||
dbPath = Directory.current.path + '/data/db/sales.db';
|
||
}
|
||
|
||
// DB ディレクトリを作成
|
||
await Directory(dbPath).parent.create(recursive: true);
|
||
|
||
_database = await _initDatabase(dbPath);
|
||
print('[DatabaseHelper] DB initialized successfully at $dbPath');
|
||
|
||
} catch (e) {
|
||
print('DB init error: $e');
|
||
throw Exception('Database initialization failed: $e');
|
||
}
|
||
}
|
||
|
||
/// テーブル作成時にサンプルデータを自動的に挿入
|
||
static Future<Database> _initDatabase(String path) async {
|
||
return await openDatabase(
|
||
path,
|
||
version: 1,
|
||
onCreate: _onCreateTableWithSampleData,
|
||
);
|
||
}
|
||
|
||
/// テーブル作成用関数 + サンプルデータ自動挿入
|
||
static Future<void> _onCreateTableWithSampleData(Database db, int version) async {
|
||
// products テーブル(Product モデルと整合性を取る)
|
||
await db.execute('''
|
||
CREATE TABLE products (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
product_code TEXT UNIQUE NOT NULL,
|
||
name TEXT NOT NULL,
|
||
unit_price REAL DEFAULT 0.0,
|
||
quantity INTEGER DEFAULT 0,
|
||
stock INTEGER DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''');
|
||
|
||
// customers テーブル
|
||
await db.execute('''
|
||
CREATE TABLE customers (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
customer_code TEXT UNIQUE NOT NULL,
|
||
name TEXT NOT NULL,
|
||
address TEXT,
|
||
phone TEXT,
|
||
email TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''');
|
||
|
||
// sales テーブル
|
||
await db.execute('''
|
||
CREATE TABLE sales (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
customer_id INTEGER,
|
||
product_id INTEGER REFERENCES products(id),
|
||
quantity INTEGER NOT NULL,
|
||
unit_price REAL NOT NULL,
|
||
total_amount REAL NOT NULL,
|
||
tax_rate REAL DEFAULT 8.0,
|
||
tax_amount REAL,
|
||
grand_total REAL NOT NULL,
|
||
status TEXT DEFAULT 'completed',
|
||
payment_status TEXT DEFAULT 'paid',
|
||
invoice_number TEXT UNIQUE,
|
||
notes TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''');
|
||
|
||
// estimates テーブル(Estimate モデルと整合性を取る)
|
||
await db.execute('''
|
||
CREATE TABLE estimates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
quote_number TEXT UNIQUE,
|
||
customer_id INTEGER REFERENCES customers(id),
|
||
product_id INTEGER REFERENCES products(id),
|
||
quantity INTEGER NOT NULL,
|
||
unit_price REAL NOT NULL,
|
||
discount_percent REAL DEFAULT 0.0,
|
||
total_amount REAL NOT NULL,
|
||
tax_rate REAL DEFAULT 8.0,
|
||
tax_amount REAL,
|
||
grand_total REAL NOT NULL,
|
||
status TEXT DEFAULT 'pending',
|
||
payment_status TEXT DEFAULT 'unpaid',
|
||
expiry_date TEXT,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
''');
|
||
|
||
// インデックス
|
||
await db.execute('CREATE INDEX idx_products_code ON products(product_code)');
|
||
await db.execute('CREATE INDEX idx_customers_code ON customers(customer_code)');
|
||
|
||
// サンプル製品データを挿入(テーブル作成時に自動的に実行)
|
||
final sampleProducts = <Map<String, dynamic>>[
|
||
{'product_code': 'TEST001', 'name': 'サンプル商品 A', 'unit_price': 1000.0, 'quantity': 50, 'stock': 50},
|
||
{'product_code': 'TEST002', 'name': 'サンプル商品 B', 'unit_price': 2500.0, 'quantity': 30, 'stock': 30},
|
||
{'product_code': 'TEST003', 'name': 'サンプル商品 C', 'unit_price': 5000.0, 'quantity': 20, 'stock': 20},
|
||
];
|
||
|
||
for (final data in sampleProducts) {
|
||
await db.insert('products', data);
|
||
}
|
||
|
||
print('[DatabaseHelper] Sample products inserted');
|
||
}
|
||
|
||
/// データベースインスタンスへのアクセス
|
||
static Database get instance => _database!;
|
||
|
||
/// 製品一覧を取得(非アクティブ除外)
|
||
static Future<List<Product>> getProducts() async {
|
||
final result = await instance.query('products', orderBy: 'id DESC');
|
||
|
||
// DateTime オブジェクトを文字列に変換してから Product からマップ
|
||
return List.generate(result.length, (index) {
|
||
final item = Map<String, dynamic>.from(result[index]);
|
||
|
||
if (item['created_at'] is DateTime) {
|
||
item['created_at'] = (item['created_at'] as DateTime).toIso8601String();
|
||
}
|
||
if (item['updated_at'] is DateTime) {
|
||
item['updated_at'] = (item['updated_at'] as DateTime).toIso8601String();
|
||
}
|
||
|
||
return Product.fromMap(item);
|
||
});
|
||
}
|
||
|
||
/// 製品を ID で取得(エラー時は null を返す)
|
||
static Future<Product?> getProduct(int id) async {
|
||
final result = await instance.query(
|
||
'products',
|
||
where: 'id = ?',
|
||
whereArgs: [id],
|
||
);
|
||
|
||
if (result.isNotEmpty) {
|
||
final item = Map<String, dynamic>.from(result[0]);
|
||
|
||
if (item['created_at'] is DateTime) {
|
||
item['created_at'] = (item['created_at'] as DateTime).toIso8601String();
|
||
}
|
||
if (item['updated_at'] is DateTime) {
|
||
item['updated_at'] = (item['updated_at'] as DateTime).toIso8601String();
|
||
}
|
||
|
||
return Product.fromMap(item);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// 製品を productCode で取得(エラー時は null を返す)
|
||
static Future<Product?> getProductByCode(String code) async {
|
||
final result = await instance.query(
|
||
'products',
|
||
where: 'product_code = ?',
|
||
whereArgs: [code],
|
||
);
|
||
|
||
if (result.isNotEmpty) {
|
||
final item = Map<String, dynamic>.from(result[0]);
|
||
|
||
if (item['created_at'] is DateTime) {
|
||
item['created_at'] = (item['created_at'] as DateTime).toIso8601String();
|
||
}
|
||
if (item['updated_at'] is DateTime) {
|
||
item['updated_at'] = (item['updated_at'] as DateTime).toIso8601String();
|
||
}
|
||
|
||
return Product.fromMap(item);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// クライアント ID での顧客検索(エラー時は null を返す)
|
||
static Future<Customer?> getCustomerById(int id) async {
|
||
final result = await instance.query(
|
||
'customers',
|
||
where: 'id = ?',
|
||
whereArgs: [id],
|
||
);
|
||
|
||
if (result.isNotEmpty) {
|
||
return Customer(
|
||
id: result[0]['id'] as int?,
|
||
customerCode: result[0]['customer_code'] as String?,
|
||
name: result[0]['name'] as String?,
|
||
address: result[0]['address'] as String?,
|
||
phone: result[0]['phone'] as String?,
|
||
email: result[0]['email'] as String?,
|
||
isInactive: (result[0]['is_inactive'] as bool?) ?? false,
|
||
);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// 製品を挿入(簡素版:return を省略)
|
||
static Future<void> insertProduct(Product product) async {
|
||
await instance.insert('products', {
|
||
'product_code': product.productCode,
|
||
'name': product.name,
|
||
'unit_price': product.unitPrice,
|
||
'quantity': product.quantity,
|
||
'stock': product.stock,
|
||
'created_at': DateTime.now().toIso8601String(),
|
||
'updated_at': DateTime.now().toIso8601String(),
|
||
});
|
||
}
|
||
|
||
/// 製品を削除(簡素版)
|
||
static Future<void> deleteProduct(int id) async {
|
||
await instance.delete('products', where: 'id = ?', whereArgs: [id]);
|
||
}
|
||
|
||
/// 顧客を挿入(簡素版:return を省略)
|
||
static Future<void> insertCustomer(Customer customer) async {
|
||
await instance.insert('customers', {
|
||
'customer_code': customer.customerCode,
|
||
'name': customer.name,
|
||
'address': customer.address,
|
||
'phone': customer.phone,
|
||
'email': customer.email,
|
||
'created_at': DateTime.now().toIso8601String(),
|
||
'updated_at': DateTime.now().toIso8601String(),
|
||
});
|
||
}
|
||
|
||
/// 顧客を削除(簡素版)
|
||
static Future<void> deleteCustomer(int id) async {
|
||
await instance.delete('customers', where: 'id = ?', whereArgs: [id]);
|
||
}
|
||
|
||
/// DB をクリア(サンプルデータは保持しない)
|
||
static Future<void> clearDatabase() async {
|
||
await instance.delete('products');
|
||
await instance.delete('customers');
|
||
await instance.delete('sales');
|
||
await instance.delete('estimates');
|
||
}
|
||
|
||
/// データベースを回復(全削除 + リセット + テーブル再作成)
|
||
static Future<void> recover() async {
|
||
try {
|
||
// 既存の DB ファイルを削除
|
||
final dbPath = Directory.current.path + '/data/db/sales.db';
|
||
final file = File(dbPath);
|
||
if (await file.exists()) {
|
||
await file.delete();
|
||
print('[DatabaseHelper] recover: DB ファイルを削除');
|
||
} else {
|
||
print('[DatabaseHelper] recover: DB ファイルが見つからない');
|
||
}
|
||
|
||
// 初期化を再実行(テーブル作成時にサンプルデータが自動的に挿入される)
|
||
await init();
|
||
} catch (e) {
|
||
print('[DatabaseHelper] recover error: $e');
|
||
}
|
||
}
|
||
|
||
/// DB パスを取得
|
||
static Future<String> getDbPath() async {
|
||
return Directory.current.path + '/data/db/sales.db';
|
||
}
|
||
} |