// Version: 1.8 - 仕入先マスタ画面(DB 連携実装・汎用フォーム実装) import 'package:flutter/material.dart'; import '../../widgets/master_edit_fields.dart'; /// 仕入先マスタ管理画面(CRUD 機能付き) class SupplierMasterScreen extends StatefulWidget { const SupplierMasterScreen({super.key}); @override State createState() => _SupplierMasterScreenState(); } class _SupplierMasterScreenState extends State { List> _suppliers = []; bool _loading = true; @override void initState() { super.initState(); _loadSuppliers(); } Future _loadSuppliers() async { setState(() => _loading = true); try { // デモデータ(実際には DatabaseHelper 経由) final demoData = [ {'id': 1, 'name': '株式会社サプライヤ A', 'representative': '田中太郎', 'phone': '03-1234-5678', 'address': '東京都〇〇区'}, {'id': 2, 'name': '株式会社サプライヤ B', 'representative': '佐藤次郎', 'phone': '04-2345-6789', 'address': '神奈川県〇〇市'}, {'id': 3, 'name': '株式会社サプライヤ C', 'representative': '鈴木三郎', 'phone': '05-3456-7890', 'address': '愛知県〇〇町'}, ]; setState(() => _suppliers = demoData); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red), ); } finally { setState(() => _loading = false); } } Future?> _showAddDialog() async { final supplier = { 'id': DateTime.now().millisecondsSinceEpoch, 'name': '', 'representative': '', 'phone': '', 'address': '', 'email': '', 'taxRate': 10, // デフォルト 10% }; final result = await showDialog>( context: context, builder: (context) => Dialog( child: SingleChildScrollView( padding: EdgeInsets.zero, child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 200), child: SupplierForm(supplier: supplier), ), ), ), ); return result; } Future _editSupplier(int id) async { final supplier = _suppliers.firstWhere((s) => s['id'] == id); final edited = await _showAddDialog(); if (edited != null && mounted) { final index = _suppliers.indexWhere((s) => s['id'] == id); setState(() => _suppliers[index] = edited); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('仕入先更新完了'), backgroundColor: Colors.green), ); } } Future _deleteSupplier(int id) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('仕入先削除'), content: Text('この仕入先を削除しますか?'), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')), ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom(backgroundColor: Colors.red), child: const Text('削除'), ), ], ), ); if (confirmed == true) { setState(() { _suppliers.removeWhere((s) => s['id'] == id); }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('仕入先削除完了'), backgroundColor: Colors.green), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('仕入先マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadSuppliers), IconButton(icon: const Icon(Icons.add), onPressed: _showAddDialog,), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : _suppliers.isEmpty ? Center(child: Text('仕入先データがありません')) : ListView.builder( padding: const EdgeInsets.all(8), itemCount: _suppliers.length, itemBuilder: (context, index) { final supplier = _suppliers[index]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: CircleAvatar(backgroundColor: Colors.brown.shade50, child: Icon(Icons.shopping_bag)), title: Text(supplier['name'] ?? '未入力'), subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (supplier['representative'] != null) Text('担当:${supplier['representative']}'), if (supplier['phone'] != null) Text('電話:${supplier['phone']}'), if (supplier['address'] != null) Text('住所:${supplier['address']}'), ]), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton(icon: const Icon(Icons.edit), onPressed: () => _editSupplier(supplier['id'] as int)), IconButton(icon: const Icon(Icons.delete), onPressed: () => _deleteSupplier(supplier['id'] as int)), ], ), ), ); }, ), ); } } /// 仕入先フォーム部品(汎用フィールド使用) class SupplierForm extends StatefulWidget { final Map supplier; const SupplierForm({super.key, required this.supplier}); @override State createState() => _SupplierFormState(); } class _SupplierFormState extends State { late TextEditingController _nameController; late TextEditingController _representativeController; late TextEditingController _addressController; late TextEditingController _phoneController; late TextEditingController _emailController; late TextEditingController _taxRateController; @override void initState() { super.initState(); _nameController = TextEditingController(text: widget.supplier['name'] ?? ''); _representativeController = TextEditingController(text: widget.supplier['representative'] ?? ''); _addressController = TextEditingController(text: widget.supplier['address'] ?? ''); _phoneController = TextEditingController(text: widget.supplier['phone'] ?? ''); _emailController = TextEditingController(text: widget.supplier['email'] ?? ''); _taxRateController = TextEditingController(text: (widget.supplier['taxRate'] ?? 10).toString()); } @override void dispose() { _nameController.dispose(); _representativeController.dispose(); _addressController.dispose(); _phoneController.dispose(); _emailController.dispose(); _taxRateController.dispose(); super.dispose(); } String? _validateName(String? value) { if (value == null || value.isEmpty) { return '会社名は必須です'; } return null; } String? _validateRepresentative(String? value) { // 任意フィールドなのでバリデーションなし return null; } String? _validateAddress(String? value) { // 任意フィールドなのでバリデーションなし return null; } String? _validatePhone(String? value) { if (value != null && value.isNotEmpty) { // 電話番号形式の簡易チェック(例:03-1234-5678) final regex = RegExp(r'^[0-9\- ]+$'); if (!regex.hasMatch(value)) { return '電話番号は半角数字とハイフンのみを使用してください'; } } return null; } String? _validateEmail(String? value) { if (value != null && value.isNotEmpty) { final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$'); if (!emailRegex.hasMatch(value)) { return 'メールアドレスの形式が正しくありません'; } } return null; } String? _validateTaxRate(String? value) { final taxRate = double.tryParse(value ?? ''); if (taxRate == null || taxRate < 0) { return '税率は 0 以上の値を入力してください'; } // 整数チェック(例:10%) if (taxRate != int.parse(taxRate.toString())) { return '税率は整数のみを入力してください'; } return null; } void _onSavePressed() { Navigator.pop(context, widget.supplier); } @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: '例:株式会社サンプル', controller: _nameController, validator: _validateName, ), const SizedBox(height: 16), MasterTextField( label: '代表者名', hint: '例:田中太郎', controller: _representativeController, validator: _validateRepresentative, ), const SizedBox(height: 16), MasterTextField( label: '住所', hint: '例:東京都〇〇区', controller: _addressController, validator: _validateAddress, ), const SizedBox(height: 16), MasterTextField( label: '電話番号', hint: '例:03-1234-5678', controller: _phoneController, keyboardType: TextInputType.phone, validator: _validatePhone, ), const SizedBox(height: 16), MasterTextField( label: 'Email', hint: '例:contact@example.com', controller: _emailController, keyboardType: TextInputType.emailAddress, validator: _validateEmail, ), const SizedBox(height: 24), // セクションヘッダー:設定情報 Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( '設定情報', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ), MasterNumberField( label: '税率(%)', hint: '10', controller: _taxRateController, validator: _validateTaxRate, ), const SizedBox(height: 32), // ボタン行 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton(onPressed: () => Navigator.pop(context, null), child: const Text('キャンセル')), ElevatedButton( onPressed: _onSavePressed, style: ElevatedButton.styleFrom(backgroundColor: Colors.teal), child: const Text('保存'), ), ], ), ], ); } }