// Version: 1.10 - 売上入力画面(簡易実装) import 'package:flutter/material.dart'; import '../services/database_helper.dart'; import '../models/product.dart'; import '../models/customer.dart'; class SalesScreen extends StatefulWidget { const SalesScreen({super.key}); @override State createState() => _SalesScreenState(); } class _SalesScreenState extends State with WidgetsBindingObserver { List products = []; List<_SaleItem> saleItems = <_SaleItem>[]; double totalAmount = 0.0; Customer? selectedCustomer; Future loadProducts() async { try { final ps = await DatabaseHelper.instance.getProducts(); if (mounted) setState(() => products = ps ?? const []); } catch (e) {} } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => loadProducts()); } Future searchProduct(String keyword) async { if (!mounted || keyword.isEmpty) return; final keywordLower = keyword.toLowerCase(); final matchedProducts = products.where((p) => (p.name?.toLowerCase() ?? '').contains(keywordLower) || (p.productCode ?? '').contains(keyword)).toList(); setState(() { for (final product in matchedProducts) { final existingItemIndex = saleItems.indexWhere((item) => item.productId == product.id); if (existingItemIndex == -1 || saleItems[existingItemIndex].quantity < 1) { saleItems.add(_SaleItem( productId: product.id ?? 0, productName: product.name ?? '', productCode: product.productCode ?? '', unitPrice: product.unitPrice ?? 0.0, quantity: 1, totalAmount: (product.unitPrice ?? 0.0), )); } else { saleItems[existingItemIndex].quantity += 1; saleItems[existingItemIndex].totalAmount = saleItems[existingItemIndex].unitPrice * saleItems[existingItemIndex].quantity; } } calculateTotal(); }); } void removeItem(int index) { if (index >= 0 && index < saleItems.length) { saleItems.removeAt(index); calculateTotal(); } } void calculateTotal() { final items = saleItems.map((item) => item.totalAmount).toList(); setState(() => totalAmount = items.fold(0, (sum, val) => sum + val)); } Future saveSale() async { if (saleItems.isEmpty || !mounted) return; try { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('売上データ保存'), content: Text('入力した商品情報を販売アシストに保存します。'), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), ElevatedButton( onPressed: () async { await DatabaseHelper.instance.insertSales({ 'id': DateTime.now().millisecondsSinceEpoch, 'customer_id': selectedCustomer?.id ?? 1, 'sale_date': DateTime.now().toIso8601String(), 'total_amount': (totalAmount * 1.1).round(), 'tax_rate': 8, }); if (mounted) ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('売上データ保存完了'), duration: Duration(seconds: 2)), ); }, child: const Text('保存'), ), ], ), ); } catch (e) { WidgetsBinding.instance.addPostFrameCallback((_) => ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red), )); } } void showInvoiceDialog() { if (saleItems.isEmpty || !mounted) return; showDialog( context: context, builder: (context) => AlertDialog( title: const Text('売上伝票'), content: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('得意先:${selectedCustomer?.name ?? '未指定'}', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), Text('商品数:${saleItems.length}'), const SizedBox(height: 4), Text('合計:¥${totalAmount.toStringAsFixed(0)}', style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.teal)), ], ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), ElevatedButton(child: const Text('閉じる'), onPressed: () => Navigator.pop(context),), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('売上入力'), actions: [ IconButton(icon: const Icon(Icons.save), onPressed: saveSale,), IconButton(icon: const Icon(Icons.print, color: Colors.blue), onPressed: () => showInvoiceDialog(),), ]), body: Column( children: [ Padding( padding: const EdgeInsets.all(16), child: Card( elevation: 4, child: Padding( padding: const EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text('レジモード', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Row(children: [Text('合計'), const Icon(Icons.payments, size: 32)]), const SizedBox(height: 4), Text('¥${totalAmount.toStringAsFixed(0)}', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.teal)), ],), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: TextField( decoration: InputDecoration(labelText: '商品検索', hintText: 'JAN コードまたは商品名を入力して選択', prefixIcon: const Icon(Icons.search)), onChanged: searchProduct, ), ), Expanded( child: saleItems.isEmpty ? const Center(child: Text('商品を登録')) : ListView.separated( itemCount: saleItems.length, itemBuilder: (context, index) { final item = saleItems[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Card( child: ListTile( leading: CircleAvatar(child: Icon(Icons.store)), title: Text(item.productName ?? ''), subtitle: Text('コード:${item.productCode} / ¥${item.totalAmount.toStringAsFixed(0)}'), trailing: IconButton(icon: const Icon(Icons.remove_circle_outline), onPressed: () => removeItem(index),), ), ), ); }, separatorBuilder: (_, __) => const Divider(), ), ), ], ), ); } } class _SaleItem { final int productId; final String productName; final String productCode; final double unitPrice; int quantity; double totalAmount; _SaleItem({ required this.productId, required this.productName, required this.productCode, required this.unitPrice, required this.quantity, required this.totalAmount, }); }