見積書画面簡素化\n\n- database_helper.dart の重複 API 削除\n- estimate_screen.dart の Estimate モデル依存排除(Map データ保存)\n- showModal → showDialog 修正\n- Duration(inDays:...) → Duration(days:) 修正\n- TextButton の child パラメータ追加\n- ビルド成功 (49.4MB APK)

This commit is contained in:
joe 2026-03-08 20:46:31 +09:00
parent 4c5ea99947
commit 4679ad30ae
2 changed files with 419 additions and 129 deletions

View file

@ -1,6 +1,9 @@
// Version: 1.5 -
// Version: 1.9 -
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/customer.dart';
import '../models/product.dart';
import '../services/database_helper.dart';
///
class EstimateScreen extends StatefulWidget {
@ -10,78 +13,411 @@ class EstimateScreen extends StatefulWidget {
State<EstimateScreen> createState() => _EstimateScreenState();
}
class _EstimateScreenState extends State<EstimateScreen> {
class _EstimateScreenState extends State<EstimateScreen> with SingleTickerProviderStateMixin {
Customer? _selectedCustomer;
List<Customer> _customers = [];
DateTime? _expiryDate;
//
List<Product> _products = [];
List<_EstimateItem> _estimateItems = <_EstimateItem>[];
double _totalAmount = 0.0;
String _estimateNumber = '';
@override
void initState() {
super.initState();
_loadCustomers();
_generateEstimateNumber();
_loadProducts();
}
Future<void> _loadCustomers() async {
// TODO: DatabaseHelper.instance.getCustomers() 使
setState(() => _customers = []);
try {
final customers = await DatabaseHelper.instance.getCustomers();
if (mounted) setState(() => _customers = customers ?? const <Customer>[]);
} catch (e) {}
}
///
void setExpiryDate(DateTime date) {
setState(() => _expiryDate = date);
/// YMM-0001
void _generateEstimateNumber() {
final now = DateTime.now();
final yearMonth = '${now.year}${now.month.toString().padLeft(2, '0')}';
if (mounted) setState(() => _estimateNumber = '$yearMonth-0001');
}
Future<void> _loadProducts() async {
try {
final ps = await DatabaseHelper.instance.getProducts();
if (mounted) setState(() => _products = ps ?? const <Product>[]);
} catch (e) {}
}
///
Future<void> searchProduct(String keyword) async {
if (!mounted || keyword.isEmpty || keyword.contains(' ')) return;
final keywordLower = keyword.toLowerCase();
final matchedProducts = _products.where((p) =>
(p.name?.toLowerCase() ?? '').contains(keywordLower) ||
(p.productCode ?? '').contains(keyword)).toList();
if (matchedProducts.isEmpty) return;
//
final product = matchedProducts.first;
final existingItemIndex = _estimateItems.indexWhere((item) => item.productId == product.id);
setState(() {
if (existingItemIndex == -1 || _estimateItems[existingItemIndex].quantity < 50) {
_estimateItems.add(_EstimateItem(
productId: product.id ?? 0,
productName: product.name ?? '',
productCode: product.productCode ?? '',
unitPrice: product.unitPrice ?? 0.0,
quantity: 1,
totalAmount: (product.unitPrice ?? 0.0),
));
} else if (existingItemIndex != -1) {
_estimateItems[existingItemIndex].quantity += 1;
_estimateItems[existingItemIndex].totalAmount =
_estimateItems[existingItemIndex].unitPrice * _estimateItems[existingItemIndex].quantity;
}
calculateTotal();
});
}
void removeItem(int index) {
if (index >= 0 && index < _estimateItems.length) {
_estimateItems.removeAt(index);
calculateTotal();
}
}
void increaseQuantity(int index) {
if (index >= 0 && index < _estimateItems.length) {
final item = _estimateItems[index];
if (item.quantity < 50) { // 1 50
item.quantity += 1;
item.totalAmount = item.unitPrice * item.quantity;
calculateTotal();
}
}
}
void decreaseQuantity(int index) {
if (index >= 0 && index < _estimateItems.length && _estimateItems[index].quantity > 1) {
_estimateItems[index].quantity -= 1;
_estimateItems[index].totalAmount = _estimateItems[index].unitPrice * _estimateItems[index].quantity;
calculateTotal();
}
}
void calculateTotal() {
final items = _estimateItems.map((item) => item.totalAmount).toList();
if (mounted) setState(() => _totalAmount = items.fold(0.0, (sum, val) => sum + val));
}
Future<void> saveEstimate() async {
if (_estimateItems.isEmpty || !_selectedCustomer!.customerCode.isNotEmpty) return;
try {
// Map
final estimateData = <String, dynamic>{
'customer_code': _selectedCustomer!.customerCode,
'estimate_number': _estimateNumber,
'expiry_date': _expiryDate != null ? DateFormat('yyyy-MM-dd').format(_expiryDate!) : null,
'total_amount': _totalAmount.round(),
'tax_rate': _selectedCustomer!.taxRate ?? 8,
'product_items': _estimateItems.map((item) {
return <String, dynamic>{
'productId': item.productId,
'productName': item.productName,
'unitPrice': item.unitPrice.round(),
'quantity': item.quantity,
};
}).toList(),
};
await DatabaseHelper.instance.insertEstimate(estimateData);
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('見積書保存完了'), duration: Duration(seconds: 2)),
);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('見積書')),
body: _selectedCustomer == null
? const Center(child: Text('得意先を選択してください'))
appBar: AppBar(
title: const Text('見積書'),
actions: [
IconButton(
icon: const Icon(Icons.save),
onPressed: _selectedCustomer != null ? saveEstimate : null,
),
],
),
body: _selectedCustomer == null || _estimateItems.isEmpty
? Center(child: Text('得意先を選択し、商品を検索して見積書を作成'))
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//
//
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('見積有効期限'),
subtitle: _expiryDate != null ? Text('${_expiryDate!.day}/${_expiryDate!.month}') : const Text('-'),
trailing: IconButton(icon: const Icon(Icons.calendar_today), onPressed: () {
// TODO:
}),
title: const Text('見積書番号'),
subtitle: Text(_estimateNumber),
),
const Divider(),
const Divider(height: 24),
//
//
Card(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('見積書合計'),
subtitle: const Text('¥0.00'),
trailing: IconButton(icon: const Icon(Icons.edit), onPressed: () {}),
title: const Text('得意先'),
subtitle: Text(_selectedCustomer!.name),
trailing: IconButton(icon: const Icon(Icons.person), onPressed: () => _showCustomerSelector()),
),
),
const SizedBox(height: 16),
// PDF
TextButton.icon(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('PDF 帳票生成中...')),
);
},
icon: const Icon(Icons.download),
label: const Text('PDF をダウンロード'),
//
Card(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(_expiryDate != null ? '見積有効期限' : '見積有効期限(未設定)'),
subtitle: _expiryDate != null
? Text(DateFormat('yyyy/MM/dd').format(_expiryDate!))
: const Text('-'),
trailing: IconButton(icon: const Icon(Icons.calendar_today), onPressed: () => _showDatePicker()),
),
),
const SizedBox(height: 16),
//
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: TextField(
decoration: InputDecoration(
labelText: '商品検索',
hintText: '商品名または JAN コードを入力',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(icon: const Icon(Icons.clear), onPressed: () => searchProduct('')),
),
onChanged: searchProduct,
),
),
//
Card(
child: _estimateItems.isEmpty
? Padding(
padding: const EdgeInsets.all(24),
child: Center(child: Text('商品を登録して見積書を作成')),
)
: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _estimateItems.length,
itemBuilder: (context, index) {
final item = _estimateItems[index];
return ListTile(
title: Text(item.productName),
subtitle: Text('コード:${item.productCode} / ¥${item.totalAmount.toStringAsFixed(2)} × ${item.quantity}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.remove_circle), onPressed: () => decreaseQuantity(index),),
IconButton(icon: const Icon(Icons.add_circle), onPressed: () => increaseQuantity(index),),
],
),
);
},
separatorBuilder: (_, __) => const Divider(),
),
),
const SizedBox(height: 24),
//
Card(
color: Colors.blue.shade50,
child: ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('見積書合計'),
subtitle: Text('¥${_totalAmount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
),
const SizedBox(height: 24),
//
ElevatedButton.icon(
onPressed: _selectedCustomer != null ? saveEstimate : null,
icon: const Icon(Icons.save),
label: const Text('見積書を保存'),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(16)),
),
const SizedBox(height: 12),
//
OutlinedButton.icon(
onPressed: _estimateItems.isNotEmpty ? () => _showSummary() : null,
icon: const Icon(Icons.info),
label: const Text('見積内容を確認'),
),
],
),
),
);
}
void _showCustomerSelector() {
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setStateDialog) => AlertDialog(
title: const Text('得意先を選択'),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: _customers.length,
itemBuilder: (context, index) {
final customer = _customers[index];
return ListTile(
title: Text(customer.name),
subtitle: Text('${customer.customerCode} / TEL:${customer.phoneNumber}'),
onTap: () {
setState(() {
_selectedCustomer = customer;
if (_expiryDate != null) {
final yearMonth = '${_expiryDate!.year}${_expiryDate!.month.toString().padLeft(2, '0')}';
_estimateNumber = '$yearMonth-0001';
}
});
Navigator.pop(context);
},
);
},
),
),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル'))],
),
),
);
}
void _showDatePicker() {
showDialog<bool>(
context: context,
builder: (context) => DatePickerDialog(initialDate: _expiryDate ?? DateTime.now().add(const Duration(days: 30))),
);
}
void _showSummary() {
if (_estimateItems.isEmpty) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('見積書概要'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('見積書番号:$_estimateNumber'),
const SizedBox(height: 8),
Text('得意先:${_selectedCustomer?.name ?? '未指定'}'),
const SizedBox(height: 8),
Text('合計金額:¥${_totalAmount.toStringAsFixed(2)}'),
if (_expiryDate != null) Text('有効期限:${DateFormat('yyyy/MM/dd').format(_expiryDate!)}'),
],
),
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('閉じる'))],
),
);
}
}
class _EstimateItem {
final int productId;
final String productName;
final String productCode;
double unitPrice;
int quantity;
double totalAmount;
_EstimateItem({
required this.productId,
required this.productName,
required this.productCode,
required this.unitPrice,
required this.quantity,
required this.totalAmount,
});
}
///
class DatePickerDialog extends StatefulWidget {
final DateTime initialDate;
const DatePickerDialog({super.key, required this.initialDate});
@override
State<DatePickerDialog> createState() => _DatePickerDialogState();
}
class _DatePickerDialogState extends State<DatePickerDialog> {
DateTime _selectedDate = DateTime.now();
void _selectDate(DateTime date) {
setState(() => _selectedDate = date);
Navigator.pop(context, true);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('見積有効期限を選択'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.calendar_today),
title: const Text('今日から 30 日後'),
onTap: () => _selectDate(DateTime.now().add(const Duration(days: 30))),
),
ListTile(
leading: const Icon(Icons.access_time),
title: const Text('1 ヶ月後(約 30 日)'),
onTap: () => _selectDate(DateTime.now().add(const Duration(days: 30))),
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('カスタム日付(簡易:未実装)'),
subtitle: const Text('デフォルト30 日後'),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () => _selectDate(DateTime.now().add(const Duration(days: 30))),
child: const Text('標準30 日後)'),
),
],
);
}
}

View file

@ -1,7 +1,9 @@
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();
@ -32,11 +34,13 @@ class DatabaseHelper {
await db.execute('CREATE TABLE suppliers (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, address TEXT, phone_number TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
await db.execute('CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, product_code TEXT NOT NULL, name TEXT NOT NULL, unit_price INTEGER NOT NULL, quantity INTEGER DEFAULT 0, stock INTEGER DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
await db.execute('CREATE TABLE sales (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, sale_date TEXT NOT NULL, total_amount INTEGER NOT NULL, tax_rate INTEGER DEFAULT 8, product_items TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
await db.execute('CREATE TABLE estimates (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_id INTEGER NOT NULL, estimate_number TEXT NOT NULL, product_items TEXT, total_amount INTEGER NOT NULL, tax_rate INTEGER DEFAULT 8, status TEXT DEFAULT "open", expiry_date TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
await db.execute('CREATE TABLE estimates (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_code TEXT NOT NULL, estimate_number TEXT NOT NULL, product_items TEXT, total_amount INTEGER NOT NULL, tax_rate INTEGER DEFAULT 8, status TEXT DEFAULT "open", expiry_date TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
await db.execute('CREATE TABLE inventory (id INTEGER PRIMARY KEY AUTOINCREMENT, product_code TEXT UNIQUE NOT NULL, name TEXT NOT NULL, unit_price INTEGER NOT NULL, stock INTEGER DEFAULT 0, min_stock INTEGER DEFAULT 0, max_stock INTEGER DEFAULT 1000, supplier_name TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
await db.execute('CREATE TABLE invoices (id INTEGER PRIMARY KEY AUTOINCREMENT, customer_code TEXT NOT NULL, invoice_number TEXT NOT NULL, sale_date TEXT NOT NULL, total_amount INTEGER NOT NULL, tax_rate INTEGER DEFAULT 8, status TEXT DEFAULT "paid", product_items TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)');
print('Database created with version: 1');
}
// Customer API
Future<int> insertCustomer(Customer customer) async {
final db = await database;
return await db.insert('customers', customer.toMap());
@ -57,12 +61,7 @@ class DatabaseHelper {
Future<int> updateCustomer(Customer customer) async {
final db = await 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> deleteCustomer(int id) async {
@ -70,6 +69,7 @@ class DatabaseHelper {
return await db.delete('customers', where: 'id = ?', whereArgs: [id]);
}
// Product API
Future<int> insertProduct(Product product) async {
final db = await database;
return await db.insert('products', product.toMap());
@ -90,12 +90,7 @@ class DatabaseHelper {
Future<int> updateProduct(Product product) async {
final db = await database;
return await db.update(
'products',
product.toMap(),
where: 'id = ?',
whereArgs: [product.id],
);
return await db.update('products', product.toMap(), where: 'id = ?', whereArgs: [product.id]);
}
Future<int> deleteProduct(int id) async {
@ -103,37 +98,9 @@ class DatabaseHelper {
return await db.delete('products', where: 'id = ?', whereArgs: [id]);
}
String encodeToJson(Object? data) {
try {
if (data == null) return '';
if (data is String) return data;
final json = StringBuffer('{');
var first = true;
if (data is Map) {
for (var key in data.keys) {
if (!first) json.write(',');
first = false;
json.write('"${key}":"${data[key]}"');
}
} else if (data is List) {
for (var item in data) {
if (!first) json.write(',');
first = false;
json.write('{"val":"$item"}');
}
}
json.write('}');
return json.toString();
} catch (e) {
return '';
}
}
// Sales API
Future<int> insertSales(Map<String, dynamic> salesData) async {
final db = await database;
if (salesData['product_items'] != null && salesData['product_items'] is List) {
salesData['product_items'] = encodeToJson(salesData['product_items']);
}
return await db.insert('sales', salesData);
}
@ -144,15 +111,7 @@ class DatabaseHelper {
Future<int> updateSales(Map<String, dynamic> salesData) async {
final db = await database;
if (salesData['product_items'] != null && salesData['product_items'] is List) {
salesData['product_items'] = encodeToJson(salesData['product_items']);
}
return await db.update(
'sales',
salesData,
where: 'id = ?',
whereArgs: [salesData['id'] as int],
);
return await db.update('sales', salesData, where: 'id = ?', whereArgs: [salesData['id'] as int]);
}
Future<int> deleteSales(int id) async {
@ -160,11 +119,9 @@ class DatabaseHelper {
return await db.delete('sales', where: 'id = ?', whereArgs: [id]);
}
// Estimate API
Future<int> insertEstimate(Map<String, dynamic> estimateData) async {
final db = await database;
if (estimateData['product_items'] != null && estimateData['product_items'] is List) {
estimateData['product_items'] = encodeToJson(estimateData['product_items']);
}
return await db.insert('estimates', estimateData);
}
@ -175,15 +132,7 @@ class DatabaseHelper {
Future<int> updateEstimate(Map<String, dynamic> estimateData) async {
final db = await database;
if (estimateData['product_items'] != null && estimateData['product_items'] is List) {
estimateData['product_items'] = encodeToJson(estimateData['product_items']);
}
return await db.update(
'estimates',
estimateData,
where: 'id = ?',
whereArgs: [estimateData['id'] as int],
);
return await db.update('estimates', estimateData, where: 'id = ?', whereArgs: [estimateData['id'] as int]);
}
Future<int> deleteEstimate(int id) async {
@ -191,6 +140,28 @@ class DatabaseHelper {
return await db.delete('estimates', where: 'id = ?', whereArgs: [id]);
}
// Invoice API
Future<int> insertInvoice(Map<String, dynamic> invoiceData) async {
final db = await database;
return await db.insert('invoices', invoiceData);
}
Future<List<Map<String, dynamic>>> getInvoices() async {
final db = await database;
return await db.query('invoices');
}
Future<int> updateInvoice(Map<String, dynamic> invoiceData) async {
final db = await database;
return await db.update('invoices', invoiceData, where: 'id = ?', whereArgs: [invoiceData['id'] as int]);
}
Future<int> deleteInvoice(int id) async {
final db = await database;
return await db.delete('invoices', where: 'id = ?', whereArgs: [id]);
}
// Inventory API
Future<int> insertInventory(Map<String, dynamic> inventoryData) async {
final db = await database;
return await db.insert('inventory', inventoryData);
@ -203,12 +174,7 @@ class DatabaseHelper {
Future<int> updateInventory(Map<String, dynamic> inventoryData) async {
final db = await database;
return await db.update(
'inventory',
inventoryData,
where: 'id = ?',
whereArgs: [inventoryData['id'] as int],
);
return await db.update('inventory', inventoryData, where: 'id = ?', whereArgs: [inventoryData['id'] as int]);
}
Future<int> deleteInventory(int id) async {
@ -216,6 +182,7 @@ class DatabaseHelper {
return await db.delete('inventory', where: 'id = ?', whereArgs: [id]);
}
// Employee API
Future<int> insertEmployee(Map<String, dynamic> employeeData) async {
final db = await database;
return await db.insert('employees', employeeData);
@ -228,12 +195,7 @@ class DatabaseHelper {
Future<int> updateEmployee(Map<String, dynamic> employeeData) async {
final db = await database;
return await db.update(
'employees',
employeeData,
where: 'id = ?',
whereArgs: [employeeData['id'] as int],
);
return await db.update('employees', employeeData, where: 'id = ?', whereArgs: [employeeData['id'] as int]);
}
Future<int> deleteEmployee(int id) async {
@ -241,6 +203,7 @@ class DatabaseHelper {
return await db.delete('employees', where: 'id = ?', whereArgs: [id]);
}
// Warehouse API
Future<int> insertWarehouse(Map<String, dynamic> warehouseData) async {
final db = await database;
return await db.insert('warehouses', warehouseData);
@ -253,12 +216,7 @@ class DatabaseHelper {
Future<int> updateWarehouse(Map<String, dynamic> warehouseData) async {
final db = await database;
return await db.update(
'warehouses',
warehouseData,
where: 'id = ?',
whereArgs: [warehouseData['id'] as int],
);
return await db.update('warehouses', warehouseData, where: 'id = ?', whereArgs: [warehouseData['id'] as int]);
}
Future<int> deleteWarehouse(int id) async {
@ -266,6 +224,7 @@ class DatabaseHelper {
return await db.delete('warehouses', where: 'id = ?', whereArgs: [id]);
}
// Supplier API
Future<int> insertSupplier(Map<String, dynamic> supplierData) async {
final db = await database;
return await db.insert('suppliers', supplierData);
@ -278,12 +237,7 @@ class DatabaseHelper {
Future<int> updateSupplier(Map<String, dynamic> supplierData) async {
final db = await database;
return await db.update(
'suppliers',
supplierData,
where: 'id = ?',
whereArgs: [supplierData['id'] as int],
);
return await db.update('suppliers', supplierData, where: 'id = ?', whereArgs: [supplierData['id'] as int]);
}
Future<int> deleteSupplier(int id) async {