// Version: 1.0.0 - EstimateScreen 見積入力画面 import 'package:flutter/material.dart'; import '../models/estimate.dart'; import '../services/database_helper.dart'; import '../models/product.dart'; /// 見積入力画面(Material Design テンプレート) class EstimateScreen extends StatefulWidget { const EstimateScreen({super.key}); @override State createState() => _EstimateScreenState(); } class _EstimateScreenState extends State { Customer? _selectedCustomer; final DatabaseHelper _db = DatabaseHelper.instance; List _products = []; List _customers = []; List _items = []; @override void initState() { super.initState(); _loadProducts(); _loadCustomers(); } Future _loadProducts() async { try { final products = await _db.getProducts(); setState(() => _products = products); } catch (e) { debugPrint('Product loading failed: $e'); } } Future _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 _showCustomerPicker() async { if (_customers.isEmpty) await _loadCustomers(); final selected = await showModalBottomSheet( 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)); } Future _saveEstimate() async { if (_items.isEmpty) return; // データベースへの保存 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() { 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('見積入力'), actions: [ IconButton( icon: const Icon(Icons.save), onPressed: _saveEstimate, ), ], ), 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)), ), ), ), ], ), ), ), 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(), ), ); } 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( 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}); }