diff --git a/lib/screens/master/inventory_master_screen.dart b/lib/screens/master/inventory_master_screen.dart index 446f9af..9838d98 100644 --- a/lib/screens/master/inventory_master_screen.dart +++ b/lib/screens/master/inventory_master_screen.dart @@ -1,7 +1,9 @@ -// Version: 1.6 - 在庫管理画面(簡易実装) +// Version: 1.7 - 在庫管理画面 import 'package:flutter/material.dart'; +import '../../models/product.dart'; +import '../../services/database_helper.dart'; -/// 在庫管理画面 +/// 在庫管理画面(新規登録・一覧表示) class InventoryMasterScreen extends StatefulWidget { const InventoryMasterScreen({super.key}); @@ -10,91 +12,241 @@ class InventoryMasterScreen extends StatefulWidget { } class _InventoryMasterScreenState extends State { - String _productName = ''; // 商品名 - int _stock = 0; // 在庫数 - int _minStock = 10; // 再仕入れ水準 - String? _supplierName; // 供給元 + List _products = []; + Map? _newInventory; // 新規登録用データ @override void initState() { super.initState(); - // TODO: DatabaseHelper.instance.getInventory() を使用 + _loadProducts(); + } + + Future _loadProducts() async { + try { + final products = await DatabaseHelper.instance.getProducts(); + if (mounted) setState(() => _products = products ?? const []); + } catch (e) {} + } + + void _submitNewInventory() async { + if (_newInventory == null || !(_newInventory!['product_code'] as String).isNotEmpty) return; + + try { + final db = await DatabaseHelper.instance.database; + // product_code の一意性チェック + final existing = await db.query('inventory', where: "product_code = ?", whereArgs: [(_newInventory!['product_code'] as String)]); + if (existing.isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('同じ JAN コードの在庫は既に存在します'), backgroundColor: Colors.orange), + ); + return; + } + + final inventoryData = { + 'product_code': _newInventory!['product_code'] as String, + 'name': _newInventory!['name'] as String? ?? '', + 'unit_price': (_newInventory!['unit_price'] as num?)?.toInt() ?? 0, + 'stock': (_newInventory!['stock'] as num?)?.toInt() ?? 0, + 'min_stock': (_newInventory!['min_stock'] as num?)?.toInt() ?? 10, + 'max_stock': (_newInventory!['max_stock'] as num?)?.toInt() ?? 1000, + 'supplier_name': _newInventory!['supplier_name'] as String? ?? '', + }; + + await DatabaseHelper.instance.insertInventory(inventoryData); + + if (mounted) ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('在庫登録完了'), backgroundColor: Colors.green), + ); + setState(() => _newInventory = null); + _loadProducts(); + } catch (e) { + if (mounted) ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('保存エラー:$e'), backgroundColor: Colors.red), + ); + } + } + + void _showAddDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('新規在庫登録'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + decoration: const InputDecoration(labelText: 'JAN コード', hintText: '例:4901234567890'), + onChanged: (v) => _newInventory?['product_code'] = v, + ), + TextField( + decoration: const InputDecoration(labelText: '商品名', hintText: '商品名を入力'), + onChanged: (v) => _newInventory?['name'] = v, + ), + TextField( + decoration: const InputDecoration(labelText: '単価(円)', hintText: '0'), + keyboardType: TextInputType.number, + onChanged: (v) => _newInventory?['unit_price'] = int.tryParse(v), + ), + TextField( + decoration: const InputDecoration(labelText: '在庫数', hintText: '0'), + keyboardType: TextInputType.number, + onChanged: (v) => _newInventory?['stock'] = int.tryParse(v), + ), + TextField( + decoration: const InputDecoration(labelText: '最低在庫', hintText: '10'), + keyboardType: TextInputType.number, + onChanged: (v) => _newInventory?['min_stock'] = int.tryParse(v), + ), + TextField( + decoration: const InputDecoration(labelText: '最大在庫', hintText: '1000'), + keyboardType: TextInputType.number, + onChanged: (v) => _newInventory?['max_stock'] = int.tryParse(v), + ), + TextField( + decoration: const InputDecoration(labelText: '仕入先', hintText: '仕入先会社名'), + onChanged: (v) => _newInventory?['supplier_name'] = v, + ), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), + ElevatedButton( + onPressed: _newInventory != null ? _submitNewInventory : null, + child: const Text('登録'), + ), + ], + ), + ); + } + + void _editInventory(int id) async { + final product = await DatabaseHelper.instance.getProduct(id); + if (product == null || mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('在庫編集'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + decoration: InputDecoration(labelText: 'JAN コード', hintText: product.productCode), + readOnly: true, + ), + TextField( + decoration: const InputDecoration(labelText: '商品名'), + controller: TextEditingController(text: product.name), + onChanged: (v) { + _updateInventory(id, {'name': v}); + }, + ), + TextField( + decoration: const InputDecoration(labelText: '単価(円)'), + keyboardType: TextInputType.number, + onChanged: (v) => _updateInventory(id, {'unit_price': int.tryParse(v)}), + ), + TextField( + decoration: const InputDecoration(labelText: '在庫数'), + keyboardType: TextInputType.number, + onChanged: (v) => _updateInventory(id, {'stock': int.tryParse(v)}), + ), + TextField( + decoration: const InputDecoration(labelText: '最低在庫'), + keyboardType: TextInputType.number, + onChanged: (v) => _updateInventory(id, {'min_stock': int.tryParse(v)}), + ), + TextField( + decoration: const InputDecoration(labelText: '最大在庫'), + keyboardType: TextInputType.number, + onChanged: (v) => _updateInventory(id, {'max_stock': int.tryParse(v)}), + ), + TextField( + decoration: const InputDecoration(labelText: '仕入先'), + onChanged: (v) => _updateInventory(id, {'supplier_name': v}), + ), + ], + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: const Text('保存'), + ), + ], + ), + ); + } + + void _updateInventory(int id, Map data) async { + final inventoryData = { + 'product_code': data['product_code'] as String?, + 'name': data['name'] as String?, + 'unit_price': data['unit_price'] as int?, + 'stock': data['stock'] as int?, + 'min_stock': data['min_stock'] as int?, + 'max_stock': data['max_stock'] as int?, + 'supplier_name': data['supplier_name'] as String?, + }; + + try { + await DatabaseHelper.instance.updateInventory(inventoryData); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('在庫更新完了'), backgroundColor: Colors.green), + ); + _loadProducts(); + } catch (e) {} } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('在庫管理')), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 商品名入力 - TextField( - decoration: const InputDecoration(hintText: '商品名', border: OutlineInputBorder()), - onChanged: (value) => setState(() => _productName = value), - ), - - const SizedBox(height: 16), - - // 在庫数入力 - TextField( - decoration: const InputDecoration(hintText: '在庫数', border: OutlineInputBorder()), - keyboardType: TextInputType.number, - onChanged: (value) => setState(() => _stock = int.tryParse(value) ?? 0), - ), - - const SizedBox(height: 16), - - // 再仕入れ水準入力 - TextField( - decoration: const InputDecoration(hintText: '再仕入れ水準', border: OutlineInputBorder()), - keyboardType: TextInputType.number, - onChanged: (value) => setState(() => _minStock = int.tryParse(value) ?? 10), - ), - - const SizedBox(height: 16), - - // 供給元入力(簡易) - TextField( - decoration: const InputDecoration(hintText: '供給元', border: OutlineInputBorder()), - onChanged: (value) => setState(() => _supplierName = value.isNotEmpty ? value : null), - ), - - const SizedBox(height: 16), - - // 在庫表示エリア(簡易) - Card( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('現在の在庫'), - subtitle: Text('${_stock}個', style: const TextStyle(fontSize: 18)), - trailing: _stock <= _minStock ? const Icon(Icons.warning, color: Colors.orange) : const SizedBox(), - ), - ), - - const SizedBox(height: 16), - - // 保存ボタン(簡易) - ElevatedButton( - onPressed: () { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('在庫データを更新しました')), - ); - } - }, - style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)), - child: const Text('更新'), - ), - - ], - ), - ), + appBar: AppBar( + title: const Text('在庫管理'), + actions: [ + IconButton(icon: const Icon(Icons.refresh), onPressed: _loadProducts,), + ], ), + body: _products.isEmpty ? Center(child: const Text('在庫データがありません')) : + SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: ListTile( + title: const Text('新規登録'), + subtitle: const Text('商品への在庫を登録'), + trailing: const Icon(Icons.add_circle), + onTap: _showAddDialog, + ), + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _products.length, + itemBuilder: (context, index) { + final product = _products[index]; + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + title: Text(product.name), + subtitle: Text('JAN: ${product.productCode} / ¥${product.unitPrice} × ${product.stock ?? 0}'), + trailing: IconButton(icon: const Icon(Icons.edit), onPressed: () => _editInventory(product.id ?? 0),), + ), + ); + }, + ), + ], + ), + ), ); } - } \ No newline at end of file