// Version: 1.16 - 売上入力画面(PDF 帳票生成簡易実装:TODO コメント化) import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'dart:convert'; import '../services/database_helper.dart' as db; 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; final NumberFormat _currencyFormatter = NumberFormat.currency(symbol: '¥', decimalDigits: 0); // 初期化時に製品リストを取得(簡易:DB の product_code は空なのでサンプルから生成) Future loadProducts() async { try { // DB から製品一覧を取得 final result = await db.DatabaseHelper.instance.query('products', orderBy: 'id DESC'); if (result.isEmpty) { // データベースに未登録の場合:簡易テストデータ products = [ Product(id: 1, productCode: 'TEST001', name: 'サンプル商品 A', unitPrice: 1000.0), Product(id: 2, productCode: 'TEST002', name: 'サンプル商品 B', unitPrice: 2500.0), ]; } else { // DB の製品データを Model に変換 products = List.generate(result.length, (i) { return Product( id: result[i]['id'] as int?, productCode: result[i]['product_code'] as String? ?? '', name: result[i]['name'] as String? ?? '', unitPrice: (result[i]['unit_price'] as num?)?.toDouble() ?? 0.0, quantity: (result[i]['quantity'] as int?) ?? 0, stock: (result[i]['stock'] as int?) ?? 0, ); }); } } catch (e) { // エラー時は空リストで初期化 products = []; } } Future refreshProducts() async { await loadProducts(); if (mounted) setState(() {}); } // Database に売上データを保存 Future saveSalesData() async { if (saleItems.isEmpty || !mounted) return; try { final itemsJson = jsonEncode(saleItems.map((item) => { 'product_id': item.productId, 'product_name': item.productName, 'product_code': item.productCode, 'unit_price': item.unitPrice.round(), 'quantity': item.quantity, 'subtotal': (item.unitPrice * item.quantity).round(), })); final salesData = { 'id': DateTime.now().millisecondsSinceEpoch, 'customer_id': selectedCustomer?.id ?? 1, 'sale_date': DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), 'total_amount': totalAmount.round(), 'tax_rate': 8, 'product_items': itemsJson, }; // sqflite の insert API を使用(insertSales は存在しない) final insertedId = await db.DatabaseHelper.instance.insert('sales', salesData); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('✅ 売上データ保存完了'), backgroundColor: Colors.green, duration: Duration(seconds: 2)), ); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('保存成功'), content: Text('売上 ID: #$insertedId\n合計金額:${_currencyFormatter.format(totalAmount)}'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('OK'), ), ], ), ); } } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('❌ 保存エラー:$e'), backgroundColor: Colors.red), ); } } // PDF 帳票生成ロジックは TODO に記述(printing パッケージ使用) Future generateAndShareInvoice() async { if (saleItems.isEmpty || !mounted) return; try { await saveSalesData(); if (!mounted) return; // TODO: PDF ファイルを生成して共有するロジックを実装(printing パッケージ使用) // 簡易実装:成功メッセージのみ表示 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('📄 売上明細が共有されました'), backgroundColor: Colors.green), ); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('共有エラー:$e'), backgroundColor: Colors.orange), ); } } 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)); } @override Widget build(BuildContext context) { // 製品リストを初期化する(1 回だけ) if (products.isEmpty) { loadProducts(); } return Scaffold( appBar: AppBar(title: const Text('/S4. 売上入力(レジ)'), actions: [ IconButton(icon: const Icon(Icons.save), onPressed: saveSalesData,), IconButton(icon: const Icon(Icons.refresh), onPressed: refreshProducts,), ]), 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('${_currencyFormatter.format(totalAmount)}', 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 ? 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} / ${_currencyFormatter.format(item.totalAmount)}'), 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, }); }