feat: 見積入力画面の商品追加機能を実装(Product CRUD)
- lib/models/product.dart: Product クラス作成 - lib/services/database_helper.dart: getProducts/insert/update/delete/Product snapshot API 追加 - lib/screens/estimate_screen.dart: 商品選択・合計計算・得意先連携ロジック実装
This commit is contained in:
parent
735687cb39
commit
57f1898656
3 changed files with 383 additions and 165 deletions
75
lib/models/product.dart
Normal file
75
lib/models/product.dart
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Version: 1.0.0
|
||||
import '../services/database_helper.dart';
|
||||
|
||||
class Product {
|
||||
int? id;
|
||||
String productCode;
|
||||
String name;
|
||||
int price; // 単価(税込)
|
||||
int stock;
|
||||
String? category;
|
||||
String? unit;
|
||||
int isDeleted = 0; // Soft delete flag
|
||||
|
||||
Product({
|
||||
this.id,
|
||||
required this.productCode,
|
||||
required this.name,
|
||||
required this.price,
|
||||
this.stock = 0,
|
||||
this.category,
|
||||
this.unit,
|
||||
this.isDeleted = 0,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'product_code': productCode,
|
||||
'name': name,
|
||||
'price': price,
|
||||
'stock': stock,
|
||||
'category': category ?? '',
|
||||
'unit': unit ?? '',
|
||||
'is_deleted': isDeleted,
|
||||
};
|
||||
}
|
||||
|
||||
factory Product.fromMap(Map<String, dynamic> map) {
|
||||
return Product(
|
||||
id: map['id'] as int?,
|
||||
productCode: map['product_code'] as String,
|
||||
name: map['name'] as String,
|
||||
price: map['price'] as int,
|
||||
stock: map['stock'] as int? ?? 0,
|
||||
category: map['category'] as String?,
|
||||
unit: map['unit'] as String?,
|
||||
isDeleted: map['is_deleted'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Product copyWith({
|
||||
int? id,
|
||||
String? productCode,
|
||||
String? name,
|
||||
int? price,
|
||||
int? stock,
|
||||
String? category,
|
||||
String? unit,
|
||||
int? isDeleted,
|
||||
}) {
|
||||
return Product(
|
||||
id: id ?? this.id,
|
||||
productCode: productCode ?? this.productCode,
|
||||
name: name ?? this.name,
|
||||
price: price ?? this.price,
|
||||
stock: stock ?? this.stock,
|
||||
category: category ?? this.category,
|
||||
unit: unit ?? this.unit,
|
||||
isDeleted: isDeleted ?? this.isDeleted,
|
||||
);
|
||||
}
|
||||
|
||||
// 税込価格(引数にない場合は自身)
|
||||
int get taxPrice => price;
|
||||
}
|
||||
|
|
@ -1,68 +1,97 @@
|
|||
// Version: 1.0.0
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/database_helper.dart';
|
||||
import '../models/product.dart';
|
||||
|
||||
/// 見積入力画面(Material Design テンプレート)
|
||||
class EstimateScreen extends StatelessWidget {
|
||||
class EstimateScreen extends StatefulWidget {
|
||||
const EstimateScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('見積入力'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: () => _showSaveDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 得意先選択
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: '得意先',
|
||||
hintText: '得意先マスタから選択',
|
||||
prefixIcon: Icon(Icons.person_search),
|
||||
),
|
||||
readOnly: true,
|
||||
onTap: () => _showCustomerPicker(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
State<EstimateScreen> createState() => _EstimateScreenState();
|
||||
}
|
||||
|
||||
// 商品追加リスト(簡易テンプレート)
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ExpansionTile(
|
||||
title: const Text('見積商品'),
|
||||
children: [
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: 0, // デモ用
|
||||
itemBuilder: (context, index) => Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
child: Icon(Icons.receipt, color: Colors.blue),
|
||||
),
|
||||
title: Text('商品${index + 1}'),
|
||||
subtitle: Text('単価:¥${(index + 1) * 1000}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
class _EstimateScreenState extends State<EstimateScreen> {
|
||||
Customer? _selectedCustomer;
|
||||
final DatabaseHelper _db = DatabaseHelper.instance;
|
||||
List<Product> _products = [];
|
||||
List<Customer> _customers = [];
|
||||
List<LineItem> _items = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
_loadCustomers();
|
||||
}
|
||||
|
||||
void _showSaveDialog(BuildContext context) {
|
||||
Future<void> _loadProducts() async {
|
||||
try {
|
||||
final products = await _db.getProducts();
|
||||
setState(() => _products = products);
|
||||
} catch (e) {
|
||||
debugPrint('Product loading failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCustomers() async {
|
||||
try {
|
||||
final customers = await _db.getCustomers();
|
||||
setState(() => _customers = customers.where((c) => c.isDeleted == 0).toList());
|
||||
} catch (e) {
|
||||
debugPrint('Customer loading failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCustomerPicker() async {
|
||||
if (_customers.isEmpty) await _loadCustomers();
|
||||
|
||||
final selected = await showModalBottomSheet<Customer>(
|
||||
context: context,
|
||||
builder: (ctx) => SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.4,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: _customers.length,
|
||||
itemBuilder: (ctx, index) => ListTile(
|
||||
title: Text(_customers[index].name),
|
||||
subtitle: Text('コード:${_customers[index].customerCode}'),
|
||||
onTap: () => Navigator.pop(ctx, _customers[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected is Customer && selected.id != _selectedCustomer?.id) {
|
||||
setState(() => _selectedCustomer = selected);
|
||||
}
|
||||
}
|
||||
|
||||
void _addSelectedProducts() async {
|
||||
for (final product in _products) {
|
||||
setState(() => _items.add(LineItem(
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
unitPrice: product.price,
|
||||
quantity: 1,
|
||||
total: product.price,
|
||||
)));
|
||||
}
|
||||
_showAddDialog();
|
||||
}
|
||||
|
||||
void _removeLineItem(int index) {
|
||||
setState(() => _items.removeAt(index));
|
||||
}
|
||||
|
||||
void _updateLineItemQuantity(int index, int quantity) {
|
||||
setState(() {
|
||||
_items[index].quantity = quantity;
|
||||
_items[index].total = quantity * _items[index].unitPrice;
|
||||
});
|
||||
}
|
||||
|
||||
void _showSaveDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
|
|
@ -71,7 +100,26 @@ class EstimateScreen extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('見積データを保存しますか?'),
|
||||
if (_selectedCustomer != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text('得意先:${_selectedCustomer!.name}'),
|
||||
),
|
||||
],
|
||||
Text('合計:¥${_calculateTotal()}'),
|
||||
if (_items.isNotEmpty) ...[
|
||||
Divider(),
|
||||
..._items.map((item) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(item.productName),
|
||||
Text('¥${item.unitPrice} × ${item.quantity} = ¥${item.total}'),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -81,10 +129,16 @@ class EstimateScreen extends StatelessWidget {
|
|||
child: const Text('キャンセル'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (_items.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('商品を追加してください')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.pop(ctx);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('見積保存しました')),
|
||||
const SnackBar(content: Text('見積保存しました'))..behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
},
|
||||
child: const Text('確定'),
|
||||
|
|
@ -94,7 +148,117 @@ class EstimateScreen extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
void _showCustomerPicker(BuildContext context) {
|
||||
// TODO: CustomerPickerModal を再利用して実装
|
||||
int _calculateTotal() {
|
||||
return _items.fold(0, (sum, item) => sum + item.total);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long, size: 64, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 16),
|
||||
Text('見積商品を追加してください', style: TextStyle(color: Colors.grey.shade600)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('見積入力')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildCustomerField(),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ExpansionTile(
|
||||
title: const Text('見積商品'),
|
||||
children: [
|
||||
if (_items.isEmpty) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _buildEmptyState(),
|
||||
),
|
||||
] else ...[
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _items.length,
|
||||
itemBuilder: (context, index) => Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
child: Icon(Icons.receipt, color: Colors.blue),
|
||||
),
|
||||
title: Text(_items[index].productName),
|
||||
subtitle: Text('単価:¥${_items[index].unitPrice}'),
|
||||
trailing: IconButton(icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _removeLineItem(index)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerField() {
|
||||
return TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: '得意先',
|
||||
hintText: _selectedCustomer != null ? _selectedCustomer.name : '得意先マスタから選択',
|
||||
prefixIcon: Icon(Icons.person_search),
|
||||
isReadOnly: true,
|
||||
),
|
||||
onTap: () => _showCustomerPicker(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddDialog() async {
|
||||
final selected = await showModalBottomSheet<Product>(
|
||||
context: context,
|
||||
builder: (ctx) => ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: _products.length,
|
||||
itemBuilder: (ctx, index) => CheckboxListTile(
|
||||
title: Text(_products[index].name),
|
||||
subtitle: Text('¥${_products[index].price} / 在庫:${_products[index].stock}${_products[index].unit ?? ''}'),
|
||||
value: _items.any((i) => i.productId == _products[index].id),
|
||||
onChanged: (value) {
|
||||
if (value) _addSelectedProducts();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected != null && selected.id != null && !_items.any((i) => i.productId == selected.id)) {
|
||||
setState(() => _items.add(LineItem(
|
||||
productId: selected.id,
|
||||
productName: selected.name,
|
||||
unitPrice: selected.price,
|
||||
quantity: 1,
|
||||
total: selected.price,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 見積行モデル
|
||||
class LineItem {
|
||||
final int? productId;
|
||||
final String productName;
|
||||
final int unitPrice;
|
||||
int quantity = 1;
|
||||
int get total => quantity * unitPrice;
|
||||
|
||||
LineItem({required this.productId, required this.productName, required this.unitPrice, this.quantity = 1});
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:sqflite/sqflite.dart';
|
|||
import 'package:path/path.dart';
|
||||
import 'dart:convert';
|
||||
import '../models/customer.dart';
|
||||
import '../models/product.dart';
|
||||
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
|
|
@ -29,7 +30,7 @@ class DatabaseHelper {
|
|||
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
const textType = 'TEXT NOT NULL';
|
||||
|
||||
|
||||
// customers テーブル作成(テストデータ挿入含む)
|
||||
await db.execute('''
|
||||
CREATE TABLE customers (
|
||||
|
|
@ -110,6 +111,16 @@ class DatabaseHelper {
|
|||
)
|
||||
''');
|
||||
|
||||
// product_snapshots テーブル(イベントソーシング用)
|
||||
await db.execute('''
|
||||
CREATE TABLE product_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_id INTEGER NOT NULL,
|
||||
data_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// テストデータ初期化(各テーブルが空の場合のみ)
|
||||
await _insertTestData(db);
|
||||
}
|
||||
|
|
@ -122,7 +133,6 @@ class DatabaseHelper {
|
|||
.toList();
|
||||
|
||||
if (existingCustomerCodes.isEmpty) {
|
||||
// テスト顧客データ
|
||||
await db.insert('customers', {
|
||||
'customer_code': 'C00001',
|
||||
'name': 'テスト株式会社 Alpha',
|
||||
|
|
@ -162,7 +172,7 @@ class DatabaseHelper {
|
|||
'updated_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
// employees テーブル(存在する場合はスキップ)
|
||||
// employees テーブル
|
||||
try {
|
||||
await db.execute('CREATE TABLE IF NOT EXISTS employees (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, position TEXT, phone_number TEXT)');
|
||||
|
||||
|
|
@ -172,27 +182,13 @@ class DatabaseHelper {
|
|||
.toList();
|
||||
|
||||
if (existingEmployees.isEmpty) {
|
||||
await db.insert('employees', {
|
||||
'name': '山田 太郎',
|
||||
'position': '営業部長',
|
||||
'phone_number': '090-1234-5678',
|
||||
});
|
||||
|
||||
await db.insert('employees', {
|
||||
'name': '鈴木 花子',
|
||||
'position': '経理部長',
|
||||
'phone_number': '090-8765-4321',
|
||||
});
|
||||
|
||||
await db.insert('employees', {
|
||||
'name': '田中 次郎',
|
||||
'position': '技術部長',
|
||||
'phone_number': '090-1111-2222',
|
||||
});
|
||||
await db.insert('employees', {'name': '山田 太郎', 'position': '営業部長', 'phone_number': '090-1234-5678'});
|
||||
await db.insert('employees', {'name': '鈴木 花子', 'position': '経理部長', 'phone_number': '090-8765-4321'});
|
||||
await db.insert('employees', {'name': '田中 次郎', 'position': '技術部長', 'phone_number': '090-1111-2222'});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// warehouses テーブル(存在する場合はスキップ)
|
||||
// warehouses テーブル
|
||||
try {
|
||||
await db.execute('CREATE TABLE IF NOT EXISTS warehouses (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, address TEXT, manager TEXT)');
|
||||
|
||||
|
|
@ -202,21 +198,12 @@ class DatabaseHelper {
|
|||
.toList();
|
||||
|
||||
if (existingWarehouses.isEmpty) {
|
||||
await db.insert('warehouses', {
|
||||
'name': '中央倉庫',
|
||||
'address': '千葉県市川市_物流センター 1-1',
|
||||
'manager': '佐藤 健一',
|
||||
});
|
||||
|
||||
await db.insert('warehouses', {
|
||||
'name': '東京支庫',
|
||||
'address': '東京都品川区_倉庫棟 2-2',
|
||||
'manager': '高橋 美咲',
|
||||
});
|
||||
await db.insert('warehouses', {'name': '中央倉庫', 'address': '千葉県市川市_物流センター 1-1', 'manager': '佐藤 健一'});
|
||||
await db.insert('warehouses', {'name': '東京支庫', 'address': '東京都品川区_倉庫棟 2-2', 'manager': '高橋 美咲'});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// suppliers テーブル(存在する場合はスキップ)
|
||||
// suppliers テーブル
|
||||
try {
|
||||
await db.execute('CREATE TABLE IF NOT EXISTS suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, tax_rate INTEGER DEFAULT 8)');
|
||||
|
||||
|
|
@ -226,24 +213,13 @@ class DatabaseHelper {
|
|||
.toList();
|
||||
|
||||
if (existingSuppliers.isEmpty) {
|
||||
await db.insert('suppliers', {
|
||||
'name': '仕入元 Alpha',
|
||||
'tax_rate': 8,
|
||||
});
|
||||
|
||||
await db.insert('suppliers', {
|
||||
'name': '仕入元 Beta',
|
||||
'tax_rate': 10,
|
||||
});
|
||||
|
||||
await db.insert('suppliers', {
|
||||
'name': '仕入元 Gamma',
|
||||
'tax_rate': 10,
|
||||
});
|
||||
await db.insert('suppliers', {'name': '仕入元 Alpha', 'tax_rate': 8});
|
||||
await db.insert('suppliers', {'name': '仕入元 Beta', 'tax_rate': 10});
|
||||
await db.insert('suppliers', {'name': '仕入元 Gamma', 'tax_rate': 10});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// products テーブル(存在する場合はスキップ)
|
||||
// products テーブル
|
||||
try {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
|
|
@ -263,37 +239,16 @@ class DatabaseHelper {
|
|||
.toList();
|
||||
|
||||
if (existingProducts.isEmpty) {
|
||||
await db.insert('products', {
|
||||
'product_code': 'PRD001',
|
||||
'name': 'テスト商品 A',
|
||||
'price': 3500,
|
||||
'stock': 100,
|
||||
'category': '食品',
|
||||
'unit': '個',
|
||||
});
|
||||
|
||||
await db.insert('products', {
|
||||
'product_code': 'PRD002',
|
||||
'name': 'テスト商品 B',
|
||||
'price': 5500,
|
||||
'stock': 50,
|
||||
'category': '家電',
|
||||
'unit': '台',
|
||||
});
|
||||
|
||||
await db.insert('products', {
|
||||
'product_code': 'PRD003',
|
||||
'name': 'テスト商品 C',
|
||||
'price': 2800,
|
||||
'stock': 200,
|
||||
'category': '文具',
|
||||
'unit': '冊',
|
||||
});
|
||||
await db.insert('products', {'product_code': 'PRD001', 'name': 'テスト商品 A', 'price': 3500, 'stock': 100, 'category': '食品', 'unit': '個'});
|
||||
await db.insert('products', {'product_code': 'PRD002', 'name': 'テスト商品 B', 'price': 5500, 'stock': 50, 'category': '家電', 'unit': '台'});
|
||||
await db.insert('products', {'product_code': 'PRD003', 'name': 'テスト商品 C', 'price': 2800, 'stock': 200, 'category': '文具', 'unit': '冊'});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Customer CRUD ==========
|
||||
|
||||
Future<int> insert(Customer customer) async {
|
||||
final db = await instance.database;
|
||||
return await db.insert('customers', customer.toMap());
|
||||
|
|
@ -311,11 +266,7 @@ class DatabaseHelper {
|
|||
|
||||
Future<Customer?> getCustomer(int id) async {
|
||||
final db = await instance.database;
|
||||
final maps = await db.query(
|
||||
'customers',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
final maps = await db.query('customers', where: 'id = ?', whereArgs: [id]);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return Customer.fromMap(maps.first);
|
||||
|
|
@ -323,22 +274,12 @@ class DatabaseHelper {
|
|||
|
||||
Future<int> update(Customer customer) async {
|
||||
final db = await instance.database;
|
||||
return await db.update(
|
||||
'customers',
|
||||
customer.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [customer.id],
|
||||
);
|
||||
return await db.update('customers', customer.toMap(), where: 'id = ?', whereArgs: [customer.id]);
|
||||
}
|
||||
|
||||
Future<int> delete(int id) async {
|
||||
final db = await instance.database;
|
||||
return await db.update(
|
||||
'customers',
|
||||
{'is_deleted': 1}, // Soft delete
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
return await db.update('customers', {'is_deleted': 1}, where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
Future<void> saveSnapshot(Customer customer) async {
|
||||
|
|
@ -346,26 +287,64 @@ class DatabaseHelper {
|
|||
final id = customer.id;
|
||||
final now = DateTime.now().toIso8601String();
|
||||
|
||||
// Check if snapshot already exists for this customer (keep last 5 snapshots)
|
||||
final existing = await db.query(
|
||||
'customer_snapshots',
|
||||
where: 'customer_id = ?',
|
||||
whereArgs: [id],
|
||||
limit: 6,
|
||||
);
|
||||
final existing = await db.query('customer_snapshots', where: 'customer_id = ?', whereArgs: [id], limit: 6);
|
||||
|
||||
if (existing.length > 5) {
|
||||
// Delete oldest snapshot
|
||||
final oldestId = existing.first['id'] as int;
|
||||
await db.delete('customer_snapshots', where: 'id = ?', whereArgs: [oldestId]);
|
||||
}
|
||||
|
||||
// Insert new snapshot
|
||||
await db.insert('customer_snapshots', {
|
||||
'customer_id': id,
|
||||
'data_json': jsonEncode(customer.toMap()),
|
||||
'created_at': now,
|
||||
});
|
||||
await db.insert('customer_snapshots', {'customer_id': id, 'data_json': jsonEncode(customer.toMap()), 'created_at': now});
|
||||
}
|
||||
|
||||
// ========== Product CRUD ==========
|
||||
|
||||
Future<int> insert(Product product) async {
|
||||
final db = await instance.database;
|
||||
return await db.insert('products', product.toMap());
|
||||
}
|
||||
|
||||
Future<List<Product>> getProducts() async {
|
||||
final db = await instance.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query('products', orderBy: 'product_code ASC');
|
||||
|
||||
return (maps as List)
|
||||
.map((json) => Product.fromMap(json))
|
||||
.where((p) => p.isDeleted == 0)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<Product?> getProduct(int id) async {
|
||||
final db = await instance.database;
|
||||
final maps = await db.query('products', where: 'id = ?', whereArgs: [id]);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return Product.fromMap(maps.first);
|
||||
}
|
||||
|
||||
Future<int> update(Product product) async {
|
||||
final db = await instance.database;
|
||||
return await db.update('products', product.toMap(), where: 'id = ?', whereArgs: [product.id]);
|
||||
}
|
||||
|
||||
Future<int> delete(int id) async {
|
||||
final db = await instance.database;
|
||||
return await db.update('products', {'is_deleted': 1}, where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
Future<void> saveSnapshot(Product product) async {
|
||||
final db = await instance.database;
|
||||
final id = product.id;
|
||||
final now = DateTime.now().toIso8601String();
|
||||
|
||||
final existing = await db.query('product_snapshots', where: 'product_id = ?', whereArgs: [id], limit: 6);
|
||||
|
||||
if (existing.length > 5) {
|
||||
final oldestId = existing.first['id'] as int;
|
||||
await db.delete('product_snapshots', where: 'id = ?', whereArgs: [oldestId]);
|
||||
}
|
||||
|
||||
await db.insert('product_snapshots', {'product_id': id, 'data_json': jsonEncode(product.toMap()), 'created_at': now});
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue