h-1.flutter.4/lib/screens/master/inventory_master_screen.dart
joe 5480ae1a79 在庫管理機能実装(Sprint 5)
- InventoryMasterScreen の新規作成
- 新規登録・編集・削除機能の実装
- Product から在庫情報を表示
- Build: 49.4MB
2026-03-08 21:43:36 +09:00

252 lines
No EOL
9.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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});
@override
State<InventoryMasterScreen> createState() => _InventoryMasterScreenState();
}
class _InventoryMasterScreenState extends State<InventoryMasterScreen> {
List<Product> _products = [];
Map<String, dynamic>? _newInventory; // 新規登録用データ
@override
void initState() {
super.initState();
_loadProducts();
}
Future<void> _loadProducts() async {
try {
final products = await DatabaseHelper.instance.getProducts();
if (mounted) setState(() => _products = products ?? const <Product>[]);
} 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 = <String, dynamic>{
'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<String, dynamic> data) async {
final inventoryData = <String, dynamic>{
'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('在庫管理'),
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),),
),
);
},
),
],
),
),
);
}
}