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:
joe 2026-03-07 17:02:01 +09:00
parent ff0fa2f745
commit b29026b469
3 changed files with 225 additions and 115 deletions

110
lib/models/estimate.dart Normal file
View 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());
}

View file

@ -1,5 +1,6 @@
// Version: 1.0.0 // Version: 1.0.0 - EstimateScreen
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/estimate.dart';
import '../services/database_helper.dart'; import '../services/database_helper.dart';
import '../models/product.dart'; import '../models/product.dart';
@ -84,68 +85,30 @@ class _EstimateScreenState extends State<EstimateScreen> {
setState(() => _items.removeAt(index)); setState(() => _items.removeAt(index));
} }
void _updateLineItemQuantity(int index, int quantity) { Future<void> _saveEstimate() async {
setState(() { if (_items.isEmpty) return;
_items[index].quantity = quantity;
_items[index].total = quantity * _items[index].unitPrice;
});
}
void _showSaveDialog() { //
showDialog( try {
context: context, final estimatedNo = 'EST-${DateTime.now().year}${DateTime.now().month.toString().padLeft(2, '0')}-${_items.length + 1}';
builder: (ctx) => AlertDialog(
title: const Text('見積確定'), await _db.insertEstimate(
content: SingleChildScrollView( estimateNo: estimatedNo,
child: Column( customerName: _selectedCustomer?.name ?? '未指定',
mainAxisSize: MainAxisSize.min, date: DateTime.now(),
children: [ items: _items,
if (_selectedCustomer != null) ...[ );
Padding(
padding: const EdgeInsets.symmetric(vertical: 8), ScaffoldMessenger.of(context).showSnackBar(
child: Text('得意先:${_selectedCustomer!.name}'), const SnackBar(content: Text('見積を保存しました'))..behavior: SnackBarBehavior.floating,
), );
], } catch (e) {
Text('合計:¥${_calculateTotal()}'), if (mounted) {
if (_items.isNotEmpty) ...[ ScaffoldMessenger.of(context).showSnackBar(
Divider(), SnackBar(content: Text('保存に失敗:$e'), backgroundColor: Colors.red),
..._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('確定'),
),
],
),
);
} }
int _calculateTotal() { int _calculateTotal() {
@ -168,7 +131,15 @@ class _EstimateScreenState extends State<EstimateScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('見積入力')), appBar: AppBar(
title: const Text('見積入力'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveEstimate,
),
],
),
body: ListView( body: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ 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(),
),
); );
} }

View file

@ -1,9 +1,10 @@
// Version: 1.0.0 // Version: 1.4 (estimate CRUD API )
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'dart:convert'; import 'dart:convert';
import '../models/customer.dart'; import '../models/customer.dart';
import '../models/product.dart'; import '../models/product.dart';
import '../models/estimate.dart';
class DatabaseHelper { class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init(); static final DatabaseHelper instance = DatabaseHelper._init();
@ -23,7 +24,7 @@ class DatabaseHelper {
return await openDatabase( return await openDatabase(
path, path,
version: 1, version: 2, // Estimate
onCreate: _createDB, 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 // customer_snapshots
await db.execute(''' await db.execute('''
CREATE TABLE customer_snapshots ( CREATE TABLE customer_snapshots (
@ -126,7 +143,6 @@ class DatabaseHelper {
} }
Future<void> _insertTestData(Database db) async { Future<void> _insertTestData(Database db) async {
// customers
final existingCustomerCodes = (await db.query('customers', columns: ['customer_code'])) final existingCustomerCodes = (await db.query('customers', columns: ['customer_code']))
.map((e) => e['customer_code'] as String?) .map((e) => e['customer_code'] as String?)
.whereType<String>() .whereType<String>()
@ -174,8 +190,6 @@ class DatabaseHelper {
// employees // employees
try { 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'])) final existingEmployees = (await db.query('employees', columns: ['name']))
.map((e) => e['name'] as String?) .map((e) => e['name'] as String?)
.whereType<String>() .whereType<String>()
@ -190,8 +204,6 @@ class DatabaseHelper {
// warehouses // warehouses
try { 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'])) final existingWarehouses = (await db.query('warehouses', columns: ['name']))
.map((e) => e['name'] as String?) .map((e) => e['name'] as String?)
.whereType<String>() .whereType<String>()
@ -205,8 +217,6 @@ class DatabaseHelper {
// suppliers // suppliers
try { 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'])) final existingSuppliers = (await db.query('suppliers', columns: ['name']))
.map((e) => e['name'] as String?) .map((e) => e['name'] as String?)
.whereType<String>() .whereType<String>()
@ -221,18 +231,6 @@ class DatabaseHelper {
// products // products
try { 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'])) final existingProducts = (await db.query('products', columns: ['product_code']))
.map((e) => e['product_code'] as String?) .map((e) => e['product_code'] as String?)
.whereType<String>() .whereType<String>()
@ -247,7 +245,7 @@ class DatabaseHelper {
} }
} }
// ========== Customer CRUD ========== // ========== Customer CRUD ======
Future<int> insert(Customer customer) async { Future<int> insert(Customer customer) async {
final db = await instance.database; final db = await instance.database;
@ -257,7 +255,7 @@ class DatabaseHelper {
Future<List<Customer>> getCustomers() async { Future<List<Customer>> getCustomers() async {
final db = await instance.database; final db = await instance.database;
final List<Map<String, dynamic>> maps = await db.query('customers'); final List<Map<String, dynamic>> maps = await db.query('customers');
return (maps as List) return (maps as List)
.map((json) => Customer.fromMap(json)) .map((json) => Customer.fromMap(json))
.where((c) => c.isDeleted == 0) .where((c) => c.isDeleted == 0)
@ -267,7 +265,7 @@ class DatabaseHelper {
Future<Customer?> getCustomer(int id) async { Future<Customer?> getCustomer(int id) async {
final db = await instance.database; 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; if (maps.isEmpty) return null;
return Customer.fromMap(maps.first); return Customer.fromMap(maps.first);
} }
@ -282,22 +280,54 @@ class DatabaseHelper {
return await db.update('customers', {'is_deleted': 1}, where: 'id = ?', whereArgs: [id]); 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 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); //
final existing = await db.query('estimates', where: 'estimate_no = ?', whereArgs: [estimateNo]);
if (existing.length > 5) { if (existing.isNotEmpty) {
final oldestId = existing.first['id'] as int; throw ArgumentError('見積番号「$estimateNo」は既に存在します');
await db.delete('customer_snapshots', where: 'id = ?', whereArgs: [oldestId]);
} }
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 { Future<int> insert(Product product) async {
final db = await instance.database; final db = await instance.database;
@ -307,7 +337,7 @@ class DatabaseHelper {
Future<List<Product>> getProducts() async { Future<List<Product>> getProducts() async {
final db = await instance.database; final db = await instance.database;
final List<Map<String, dynamic>> maps = await db.query('products', orderBy: 'product_code ASC'); final List<Map<String, dynamic>> maps = await db.query('products', orderBy: 'product_code ASC');
return (maps as List) return (maps as List)
.map((json) => Product.fromMap(json)) .map((json) => Product.fromMap(json))
.where((p) => p.isDeleted == 0) .where((p) => p.isDeleted == 0)
@ -317,7 +347,7 @@ class DatabaseHelper {
Future<Product?> getProduct(int id) async { Future<Product?> getProduct(int id) async {
final db = await instance.database; final db = await instance.database;
final maps = await db.query('products', where: 'id = ?', whereArgs: [id]); final maps = await db.query('products', where: 'id = ?', whereArgs: [id]);
if (maps.isEmpty) return null; if (maps.isEmpty) return null;
return Product.fromMap(maps.first); return Product.fromMap(maps.first);
} }
@ -332,21 +362,6 @@ class DatabaseHelper {
return await db.update('products', {'is_deleted': 1}, where: 'id = ?', whereArgs: [id]); 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 { Future<void> close() async {
final db = await instance.database; final db = await instance.database;
db.close(); db.close();