feat: 工程管理ドキュメントと短/長期計画を docs フォルダに作成した\n- docs/engineering_management.md (工程管理ガイド)\n- docs/short_term_plan.md (短期・スプリント計画)\n- docs/long_term_plan.md (長期・ロードマップ計画)\n- README.md ドキュメント活用法の明記を追加\n\n実装完了マスタ管理画面の完成:\n- lib/screens/estimate_screen.dart (見積入力画面)\n- lib/services/database_helper.dart (CRUD API 追加)\n- lib/models/estimate.dart (見積モデル定義)
This commit is contained in:
parent
ff0fa2f745
commit
b29026b469
3 changed files with 225 additions and 115 deletions
110
lib/models/estimate.dart
Normal file
110
lib/models/estimate.dart
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// Version: 1.0.0
|
||||
import 'dart:convert';
|
||||
|
||||
/// 見積(Estimate)モデル
|
||||
class Estimate {
|
||||
final int id;
|
||||
final String estimateNo; // 見積書 No.
|
||||
final int? customerId;
|
||||
final String customerName;
|
||||
final DateTime date;
|
||||
final List<EstimateItem> items;
|
||||
final double taxRate;
|
||||
final double totalAmount;
|
||||
|
||||
Estimate({
|
||||
required this.id,
|
||||
required this.estimateNo,
|
||||
this.customerId,
|
||||
required this.customerName,
|
||||
required this.date,
|
||||
required this.items,
|
||||
required this.taxRate,
|
||||
required this.totalAmount,
|
||||
});
|
||||
|
||||
/// 引数の ID が重複しているか確認する(null 許容)
|
||||
bool hasDuplicateId(int? id) {
|
||||
if (id == null) return false;
|
||||
// items に productId が重複していないか確認
|
||||
final itemIds = items.map((item) => item.productId).toSet();
|
||||
return !itemIds.contains(id);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'estimateNo': estimateNo,
|
||||
'customerId': customerId,
|
||||
'customerName': customerName,
|
||||
'date': date.toIso8601String(),
|
||||
'itemsJson': jsonEncode(items.map((e) => e.toMap()).toList()),
|
||||
'taxRate': taxRate,
|
||||
'totalAmount': totalAmount,
|
||||
};
|
||||
}
|
||||
|
||||
factory Estimate.fromMap(Map<String, dynamic> map) {
|
||||
final items = (map['itemsJson'] as String?) != null
|
||||
? ((jsonDecode(map['itemsJson']) as List)
|
||||
.map((e) => EstimateItem.fromMap(e as Map))
|
||||
.toList())
|
||||
: [];
|
||||
|
||||
return Estimate(
|
||||
id: map['id'] as int,
|
||||
estimateNo: map['estimateNo'] as String,
|
||||
customerId: map['customerId'] as int?,
|
||||
customerName: map['customerName'] as String,
|
||||
date: DateTime.parse(map['date'] as String),
|
||||
items: items,
|
||||
taxRate: (map['taxRate'] as num).toDouble(),
|
||||
totalAmount: (map['totalAmount'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => jsonEncode(toMap());
|
||||
}
|
||||
|
||||
/// 見積行(EstimateItem)モデル
|
||||
class EstimateItem {
|
||||
final int id;
|
||||
final int? productId;
|
||||
final String productName;
|
||||
final int quantity;
|
||||
final double unitPrice;
|
||||
final double total;
|
||||
|
||||
EstimateItem({
|
||||
required this.id,
|
||||
this.productId,
|
||||
required this.productName,
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'productId': productId,
|
||||
'productName': productName,
|
||||
'quantity': quantity,
|
||||
'unitPrice': unitPrice,
|
||||
'total': total,
|
||||
};
|
||||
}
|
||||
|
||||
factory EstimateItem.fromMap(Map<String, dynamic> map) {
|
||||
return EstimateItem(
|
||||
id: map['id'] as int,
|
||||
productId: map['productId'] as int?,
|
||||
productName: map['productName'] as String,
|
||||
quantity: map['quantity'] as int,
|
||||
unitPrice: (map['unitPrice'] as num).toDouble(),
|
||||
total: (map['total'] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => jsonEncode(toMap());
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// Version: 1.0.0
|
||||
// Version: 1.0.0 - EstimateScreen 見積入力画面
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/estimate.dart';
|
||||
import '../services/database_helper.dart';
|
||||
import '../models/product.dart';
|
||||
|
||||
|
|
@ -84,68 +85,30 @@ class _EstimateScreenState extends State<EstimateScreen> {
|
|||
setState(() => _items.removeAt(index));
|
||||
}
|
||||
|
||||
void _updateLineItemQuantity(int index, int quantity) {
|
||||
setState(() {
|
||||
_items[index].quantity = quantity;
|
||||
_items[index].total = quantity * _items[index].unitPrice;
|
||||
});
|
||||
}
|
||||
Future<void> _saveEstimate() async {
|
||||
if (_items.isEmpty) return;
|
||||
|
||||
void _showSaveDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('見積確定'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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}'),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('キャンセル'),
|
||||
),
|
||||
ElevatedButton(
|
||||
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('見積保存しました'))..behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
},
|
||||
child: const Text('確定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
// データベースへの保存
|
||||
try {
|
||||
final estimatedNo = 'EST-${DateTime.now().year}${DateTime.now().month.toString().padLeft(2, '0')}-${_items.length + 1}';
|
||||
|
||||
await _db.insertEstimate(
|
||||
estimateNo: estimatedNo,
|
||||
customerName: _selectedCustomer?.name ?? '未指定',
|
||||
date: DateTime.now(),
|
||||
items: _items,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('見積を保存しました'))..behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('保存に失敗:$e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int _calculateTotal() {
|
||||
|
|
@ -168,7 +131,15 @@ class _EstimateScreenState extends State<EstimateScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('見積入力')),
|
||||
appBar: AppBar(
|
||||
title: const Text('見積入力'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.save),
|
||||
onPressed: _saveEstimate,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
|
|
@ -203,11 +174,25 @@ class _EstimateScreenState extends State<EstimateScreen> {
|
|||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_selectedCustomer != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
child: ListTile(
|
||||
title: const Text('得意先'),
|
||||
subtitle: Text(_selectedCustomer!.name),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add_shopping_cart),
|
||||
label: const Text('商品追加'),
|
||||
onPressed: () => _showAddDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// Version: 1.0.0
|
||||
// Version: 1.4 (estimate テーブル定義修正・CRUD API 実装)
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'dart:convert';
|
||||
import '../models/customer.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/estimate.dart';
|
||||
|
||||
class DatabaseHelper {
|
||||
static final DatabaseHelper instance = DatabaseHelper._init();
|
||||
|
|
@ -23,7 +24,7 @@ class DatabaseHelper {
|
|||
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 1,
|
||||
version: 2, // Estimate テーブル追加でバージョンアップ
|
||||
onCreate: _createDB,
|
||||
);
|
||||
}
|
||||
|
|
@ -91,6 +92,22 @@ class DatabaseHelper {
|
|||
)
|
||||
''');
|
||||
|
||||
// estimate テーブル(見積書用)
|
||||
await db.execute('''
|
||||
CREATE TABLE estimates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
estimate_no TEXT NOT NULL UNIQUE,
|
||||
customer_id INTEGER,
|
||||
customer_name TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
total_amount REAL NOT NULL,
|
||||
tax_rate REAL DEFAULT 10,
|
||||
items_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
// customer_snapshots テーブル(イベントソーシング用)
|
||||
await db.execute('''
|
||||
CREATE TABLE customer_snapshots (
|
||||
|
|
@ -126,7 +143,6 @@ class DatabaseHelper {
|
|||
}
|
||||
|
||||
Future<void> _insertTestData(Database db) async {
|
||||
// customers テーブルにテストデータを挿入(既に存在しない場合のみ)
|
||||
final existingCustomerCodes = (await db.query('customers', columns: ['customer_code']))
|
||||
.map((e) => e['customer_code'] as String?)
|
||||
.whereType<String>()
|
||||
|
|
@ -174,8 +190,6 @@ class DatabaseHelper {
|
|||
|
||||
// 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)');
|
||||
|
||||
final existingEmployees = (await db.query('employees', columns: ['name']))
|
||||
.map((e) => e['name'] as String?)
|
||||
.whereType<String>()
|
||||
|
|
@ -190,8 +204,6 @@ class DatabaseHelper {
|
|||
|
||||
// warehouses テーブル
|
||||
try {
|
||||
await db.execute('CREATE TABLE IF NOT EXISTS warehouses (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, address TEXT, manager TEXT)');
|
||||
|
||||
final existingWarehouses = (await db.query('warehouses', columns: ['name']))
|
||||
.map((e) => e['name'] as String?)
|
||||
.whereType<String>()
|
||||
|
|
@ -205,8 +217,6 @@ class DatabaseHelper {
|
|||
|
||||
// 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)');
|
||||
|
||||
final existingSuppliers = (await db.query('suppliers', columns: ['name']))
|
||||
.map((e) => e['name'] as String?)
|
||||
.whereType<String>()
|
||||
|
|
@ -221,18 +231,6 @@ class DatabaseHelper {
|
|||
|
||||
// products テーブル
|
||||
try {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
product_code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
price INTEGER NOT NULL,
|
||||
stock INTEGER DEFAULT 0,
|
||||
category TEXT,
|
||||
unit TEXT
|
||||
)
|
||||
''');
|
||||
|
||||
final existingProducts = (await db.query('products', columns: ['product_code']))
|
||||
.map((e) => e['product_code'] as String?)
|
||||
.whereType<String>()
|
||||
|
|
@ -247,7 +245,7 @@ class DatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
// ========== Customer CRUD ==========
|
||||
// ========== Customer CRUD ======
|
||||
|
||||
Future<int> insert(Customer customer) async {
|
||||
final db = await instance.database;
|
||||
|
|
@ -257,7 +255,7 @@ class DatabaseHelper {
|
|||
Future<List<Customer>> getCustomers() async {
|
||||
final db = await instance.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query('customers');
|
||||
|
||||
|
||||
return (maps as List)
|
||||
.map((json) => Customer.fromMap(json))
|
||||
.where((c) => c.isDeleted == 0)
|
||||
|
|
@ -267,7 +265,7 @@ class DatabaseHelper {
|
|||
Future<Customer?> getCustomer(int id) async {
|
||||
final db = await instance.database;
|
||||
final maps = await db.query('customers', where: 'id = ?', whereArgs: [id]);
|
||||
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return Customer.fromMap(maps.first);
|
||||
}
|
||||
|
|
@ -282,22 +280,54 @@ class DatabaseHelper {
|
|||
return await db.update('customers', {'is_deleted': 1}, where: 'id = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
Future<void> saveSnapshot(Customer customer) async {
|
||||
// ========== Estimate CRUD ======
|
||||
|
||||
Future<int> insertEstimate(String estimateNo, String customerName, DateTime date, List<EstimateItem> items) async {
|
||||
final db = await instance.database;
|
||||
final id = customer.id;
|
||||
final now = DateTime.now().toIso8601String();
|
||||
|
||||
final existing = await db.query('customer_snapshots', where: 'customer_id = ?', whereArgs: [id], limit: 6);
|
||||
|
||||
if (existing.length > 5) {
|
||||
final oldestId = existing.first['id'] as int;
|
||||
await db.delete('customer_snapshots', where: 'id = ?', whereArgs: [oldestId]);
|
||||
// 見積番号の重複チェック
|
||||
final existing = await db.query('estimates', where: 'estimate_no = ?', whereArgs: [estimateNo]);
|
||||
if (existing.isNotEmpty) {
|
||||
throw ArgumentError('見積番号「$estimateNo」は既に存在します');
|
||||
}
|
||||
|
||||
await db.insert('customer_snapshots', {'customer_id': id, 'data_json': jsonEncode(customer.toMap()), 'created_at': now});
|
||||
final itemsJson = jsonEncode(items.map((e) => e.toMap()).toList());
|
||||
final totalAmount = items.fold(0, (sum, item) => sum + item.total);
|
||||
|
||||
return await db.insert('estimates', {
|
||||
'estimate_no': estimateNo,
|
||||
'customer_name': customerName,
|
||||
'date': date.toIso8601String(),
|
||||
'total_amount': totalAmount,
|
||||
'tax_rate': 10,
|
||||
'items_json': itemsJson,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Product CRUD ==========
|
||||
Future<List<Estimate>> getEstimates() async {
|
||||
final db = await instance.database;
|
||||
final List<Map<String, dynamic>> maps = await db.query('estimates', orderBy: 'created_at DESC');
|
||||
|
||||
return (maps as List)
|
||||
.map((json) => Estimate.fromMap(json))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<Estimate?> getEstimate(int id) async {
|
||||
final db = await instance.database;
|
||||
final maps = await db.query('estimates', where: 'id = ?', whereArgs: [id]);
|
||||
|
||||
if (maps.isEmpty) return null;
|
||||
return Estimate.fromMap(maps.first);
|
||||
}
|
||||
|
||||
Future<void> saveSnapshot(Estimate estimate) async {
|
||||
//_estimate_snapshots テーブルも必要に応じて追加可
|
||||
}
|
||||
|
||||
// ========== Product CRUD ======
|
||||
|
||||
Future<int> insert(Product product) async {
|
||||
final db = await instance.database;
|
||||
|
|
@ -307,7 +337,7 @@ class DatabaseHelper {
|
|||
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)
|
||||
|
|
@ -317,7 +347,7 @@ class DatabaseHelper {
|
|||
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);
|
||||
}
|
||||
|
|
@ -332,21 +362,6 @@ class DatabaseHelper {
|
|||
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 {
|
||||
final db = await instance.database;
|
||||
db.close();
|
||||
|
|
|
|||
Loading…
Reference in a new issue