h-1.flutter.4/lib/screens/master/product_master_screen.dart

293 lines
No EOL
9.4 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.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<ProductMasterScreen> createState() => _ProductMasterScreenState();
}
class _ProductMasterScreenState extends State<ProductMasterScreen> {
List<Product> _products = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadProducts();
}
Future<void> _loadProducts() async {
setState(() => _loading = true);
try {
final products = await DatabaseHelper.instance.getProducts();
if (mounted) setState(() => _products = products ?? const <Product>[]);
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red),
);
} finally {
setState(() => _loading = false);
}
}
Future<Product?> _showProductDialog({Product? initialProduct}) async {
final titleText = initialProduct == null ? '新規商品登録' : '商品編集';
return await showDialog<Product>(
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<void> _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<void> _onDeletePressed(int id) async {
final product = await DatabaseHelper.instance.getProduct(id);
if (!mounted) return;
final confirmed = await showDialog<bool>(
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<ProductForm> createState() => _ProductFormState();
}
class _ProductFormState extends State<ProductForm> {
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,
),
],
);
}
}