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 '../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(),
),
);
}

View file

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