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

341 lines
No EOL
11 KiB
Dart
Raw Permalink 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.8 - 仕入先マスタ画面DB 連携実装・汎用フォーム実装)
import 'package:flutter/material.dart';
import '../../widgets/master_edit_fields.dart';
/// 仕入先マスタ管理画面CRUD 機能付き)
class SupplierMasterScreen extends StatefulWidget {
const SupplierMasterScreen({super.key});
@override
State<SupplierMasterScreen> createState() => _SupplierMasterScreenState();
}
class _SupplierMasterScreenState extends State<SupplierMasterScreen> {
List<Map<String, dynamic>> _suppliers = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadSuppliers();
}
Future<void> _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<Map<String, dynamic>?> _showAddDialog() async {
final supplier = <String, dynamic>{
'id': DateTime.now().millisecondsSinceEpoch,
'name': '',
'representative': '',
'phone': '',
'address': '',
'email': '',
'taxRate': 10, // デフォルト 10%
};
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (context) => Dialog(
child: SingleChildScrollView(
padding: EdgeInsets.zero,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: SupplierForm(supplier: supplier),
),
),
),
);
return result;
}
Future<void> _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<void> _deleteSupplier(int id) async {
final confirmed = await showDialog<bool>(
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('/M3. 仕入先マスタ'),
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<String, dynamic> supplier;
const SupplierForm({super.key, required this.supplier});
@override
State<SupplierForm> createState() => _SupplierFormState();
}
class _SupplierFormState extends State<SupplierForm> {
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('保存'),
),
],
),
],
);
}
}