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