// Version: 1.9 - 商品マスタ画面(汎用フォーム実装) import 'package:flutter/material.dart'; import '../../models/product.dart'; import '../../services/database_helper.dart'; import '../../widgets/master_edit_fields.dart'; /// 商品マスタ管理画面(CRUD 機能付き・汎用フォーム実装) class ProductMasterScreen extends StatefulWidget { const ProductMasterScreen({super.key}); @override State createState() => _ProductMasterScreenState(); } class _ProductMasterScreenState extends State { List _products = []; bool _loading = true; @override void initState() { super.initState(); _loadProducts(); } Future _loadProducts() async { setState(() => _loading = true); try { final products = await DatabaseHelper.instance.getProducts(); if (mounted) setState(() => _products = products ?? const []); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red), ); } finally { setState(() => _loading = false); } } Future _showProductDialog({Product? initialProduct}) async { final titleText = initialProduct == null ? '新規商品登録' : '商品編集'; return await showDialog( context: context, builder: (context) => AlertDialog( title: Text(titleText), content: SingleChildScrollView(child: ProductForm(initialProduct: initialProduct)), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), ElevatedButton( onPressed: () => Navigator.pop(context, initialProduct ?? null), style: ElevatedButton.styleFrom(backgroundColor: Colors.teal), child: initialProduct == null ? const Text('登録') : const Text('更新'), ), ], ), ); } void _onAddPressed() async { final result = await _showProductDialog(); if (result != null && mounted) { try { await DatabaseHelper.instance.insertProduct(result); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('商品登録完了'), backgroundColor: Colors.green), ); _loadProducts(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red), ); } } } Future _onEditPressed(int id) async { final product = await DatabaseHelper.instance.getProduct(id); if (product == null || !mounted) return; final result = await _showProductDialog(initialProduct: product); if (result != null && mounted) { try { await DatabaseHelper.instance.updateProduct(result); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('商品更新完了'), backgroundColor: Colors.green), ); _loadProducts(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red), ); } } } Future _onDeletePressed(int id) async { final product = await DatabaseHelper.instance.getProduct(id); if (!mounted) return; final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('商品削除'), content: Text('"${product?.name ?? 'この商品'}"を削除しますか?'), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), ElevatedButton( onPressed: () { if (mounted) Navigator.pop(context, true); }, style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('削除'), ), ], ), ); if (confirmed == true && mounted) { try { await DatabaseHelper.instance.deleteProduct(id); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('商品削除完了'), backgroundColor: Colors.green), ); _loadProducts(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('削除エラー:$e'), backgroundColor: Colors.red), ); } } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('/M1. 商品マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadProducts,), IconButton(icon: const Icon(Icons.add), onPressed: _onAddPressed,), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : _products.isEmpty ? Center(child: Text('商品データがありません')) : ListView.builder( padding: const EdgeInsets.all(8), itemCount: _products.length, itemBuilder: (context, index) { final product = _products[index]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar(backgroundColor: Colors.blue.shade50, child: Icon(Icons.shopping_basket)), title: Text(product.name.isEmpty ? '商品(未入力)' : product.name), subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('コード:${product.productCode}'), Text('単価:¥${(product.unitPrice ?? 0).toStringAsFixed(2)}'), ]), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton(icon: const Icon(Icons.edit), onPressed: () => _onEditPressed(product.id ?? 0)), IconButton(icon: const Icon(Icons.delete), onPressed: () => _onDeletePressed(product.id ?? 0)), ], ), ), ); }, ), ); } } /// 商品フォーム部品(汎用フォーム実装) class ProductForm extends StatefulWidget { final Product? initialProduct; const ProductForm({super.key, this.initialProduct}); @override State createState() => _ProductFormState(); } class _ProductFormState extends State { late TextEditingController _productCodeController; late TextEditingController _nameController; late TextEditingController _unitPriceController; @override void initState() { super.initState(); final initialProduct = widget.initialProduct; _productCodeController = TextEditingController(text: initialProduct?.productCode ?? ''); _nameController = TextEditingController(text: initialProduct?.name ?? ''); _unitPriceController = TextEditingController(text: (initialProduct?.unitPrice ?? 0.0).toString()); if (_productCodeController.text.isEmpty) { _productCodeController = TextEditingController(); } if (_nameController.text.isEmpty) { _nameController = TextEditingController(); } if (_unitPriceController.text.isEmpty) { _unitPriceController = TextEditingController(text: '0'); } } @override void dispose() { _productCodeController.dispose(); _nameController.dispose(); _unitPriceController.dispose(); super.dispose(); } String? _validateProductCode(String? value) { if (value == null || value.isEmpty) { return '商品コードは必須です'; } final regex = RegExp(r'^[0-9]+$'); if (!regex.hasMatch(value)) { return '商品コードは数字のみを入力してください(例:9000)'; } return null; } String? _validateName(String? value) { if (value == null || value.isEmpty) { return '品名は必須です'; } return null; } String? _validateUnitPrice(String? value) { final price = double.tryParse(value ?? ''); if (price == null) { return '単価は数値を入力してください'; } if (price < 0) { return '単価は 0 以上の値です'; } return null; } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // セクションヘッダー:基本情報 Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( '基本情報', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), MasterTextField( label: '商品コード', hint: '例:9000', controller: _productCodeController, validator: _validateProductCode, ), const SizedBox(height: 16), MasterTextField( label: '品名', hint: '商品の名称', controller: _nameController, ), const SizedBox(height: 16), MasterNumberField( label: '単価(円)', hint: '0', controller: _unitPriceController, validator: _validateUnitPrice, ), ], ); } }