h-1.flutter.4/lib/screens/estimate_screen.dart
joe 57f1898656 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: 商品選択・合計計算・得意先連携ロジック実装
2026-03-07 15:24:15 +09:00

264 lines
No EOL
8.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Version: 1.0.0
import 'package:flutter/material.dart';
import '../services/database_helper.dart';
import '../models/product.dart';
/// 見積入力画面Material Design テンプレート)
class EstimateScreen extends StatefulWidget {
const EstimateScreen({super.key});
@override
State<EstimateScreen> createState() => _EstimateScreenState();
}
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();
}
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(
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('確定'),
),
],
),
);
}
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});
}