h-1.flutter.4/lib/screens/master/customer_master_screen.dart
joe 13f7e3fcc6 feat: マスタ編集モジュール統合と汎用フィールド実装
- widgets ディレクトリに MasterTextField, MasterNumberField, MasterDropdownField,
  MasterTextArea, MasterCheckBox を作成
- 各マスタ画面(product, customer, employee, supplier, warehouse)で統一ウィジェット化
- pubspec.yaml: flutter_form_builder の依存を整理(Flutter の標準機能で対応可能に)
2026-03-09 22:49:39 +09:00

327 lines
No EOL
13 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 - 得意先マスタ画面DB 連携実装)
import 'package:flutter/material.dart';
import '../../models/customer.dart';
import '../../services/database_helper.dart';
/// 得意先マスタ管理画面CRUD 機能付き)
class CustomerMasterScreen extends StatefulWidget {
const CustomerMasterScreen({super.key});
@override
State<CustomerMasterScreen> createState() => _CustomerMasterScreenState();
}
class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
final DatabaseHelper _db = DatabaseHelper.instance;
List<Customer> _customers = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadCustomers();
}
Future<void> _loadCustomers() async {
try {
final customers = await _db.getCustomers();
setState(() {
_customers = customers ?? const <Customer>[];
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('顧客データを読み込みませんでした:$e'), backgroundColor: Colors.red),
);
}
}
Future<void> _addCustomer(Customer customer) async {
try {
await DatabaseHelper.instance.insertCustomer(customer);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を登録しました'), backgroundColor: Colors.green),
);
_loadCustomers();
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('登録に失敗:$e'), backgroundColor: Colors.red),
);
}
}
Future<void> _editCustomer(Customer customer) async {
if (!mounted) return;
final updatedCustomer = await _showEditDialog(context, customer);
if (updatedCustomer != null && mounted) {
try {
await DatabaseHelper.instance.updateCustomer(updatedCustomer);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を更新しました'), backgroundColor: Colors.green),
);
_loadCustomers();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('更新に失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
Future<void> _deleteCustomer(int id) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客削除'),
content: Text('この顧客を削除しますか?履歴データも消去されます。'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('削除'),
),
],
),
);
if (confirmed == true) {
try {
await DatabaseHelper.instance.deleteCustomer(id);
if (mounted) _loadCustomers();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を削除しました'), backgroundColor: Colors.green),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('削除に失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
Future<Customer?> _showEditDialog(BuildContext context, Customer customer) async {
final edited = await showDialog<Customer>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客編集'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
decoration: InputDecoration(labelText: '得意先コード', hintText: customer.customerCode ?? ''),
controller: TextEditingController(text: customer.customerCode),
),
const SizedBox(height: 8),
TextField(
decoration: InputDecoration(labelText: '名称 *'),
controller: TextEditingController(text: customer.name),
onChanged: (v) => customer.name = v,
),
TextField(decoration: InputDecoration(labelText: '電話番号', hintText: '03-1234-5678')),
const SizedBox(height: 8),
TextField(
decoration: InputDecoration(labelText: 'Email'),
controller: TextEditingController(text: customer.email ?? ''),
onChanged: (v) => customer.email = v,
),
TextField(decoration: InputDecoration(labelText: '住所', hintText: '〒000-0000 市区町村名・番地')),
const SizedBox(height: 8),
TextField(
decoration: InputDecoration(labelText: '消費税率 *'),
keyboardType: TextInputType.number,
controller: TextEditingController(text: customer.taxRate.toString()),
onChanged: (v) => customer.taxRate = int.tryParse(v) ?? customer.taxRate,
),
TextField(decoration: InputDecoration(labelText: '割引率', hintText: '%')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: '担当者 ID')),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, null), child: const Text('キャンセル')),
ElevatedButton(onPressed: () => Navigator.pop(ctx, customer), child: const Text('保存')),
],
),
);
return edited;
}
Future<void> _showCustomerDetail(BuildContext context, Customer customer) async {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客詳細'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_detailRow('得意先コード', customer.customerCode),
_detailRow('名称', customer.name),
if (customer.phoneNumber != null) _detailRow('電話番号', customer.phoneNumber),
_detailRow('Email', customer.email ?? '-'),
_detailRow('住所', customer.address ?? '-'),
_detailRow('消費税率', '${customer.taxRate}%'),
_detailRow('割引率', '${customer.discountRate}%'),
],
),
),
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('閉じる'))],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 100),
Expanded(child: Text(value)),
],
),
);
}
void _showSnackBar(BuildContext context, String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('得意先マスタ'),
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _loadCustomers)],
),
body: _isLoading ? const Center(child: CircularProgressIndicator()) :
_customers.isEmpty ? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
SizedBox(height: 16),
Text('顧客データがありません', style: TextStyle(color: Colors.grey)),
SizedBox(height: 16),
FloatingActionButton.extended(
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
label: const Text('新規登録'),
onPressed: () => _showAddDialog(context),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _customers.length,
itemBuilder: (context, index) {
final customer = _customers[index];
return Dismissible(
key: Key(customer.customerCode),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) => _deleteCustomer(customer.id ?? 0),
child: Card(
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: ListTile(
leading: CircleAvatar(backgroundColor: Colors.blue.shade100, child: const Icon(Icons.person, color: Colors.blue)),
title: Text(customer.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (customer.email != null) Text('Email: ${customer.email}', style: const TextStyle(fontSize: 12)),
Text('税抜:${(customer.taxRate / 8 * 100).toStringAsFixed(1)}%'),
Text('割引:${customer.discountRate}%'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _editCustomer(customer)),
IconButton(icon: const Icon(Icons.more_vert), onPressed: () => _showMoreOptions(context, customer)),
],
),
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('新規登録'),
onPressed: () => _showAddDialog(context),
),
);
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規顧客登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(decoration: InputDecoration(labelText: '得意先コード *', hintText: 'JAN 形式など(半角数字)')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: '顧客名称 *', hintText: '株式会社〇〇')),
TextField(decoration: InputDecoration(labelText: '電話番号', hintText: '03-1234-5678')),
const SizedBox(height: 8),
TextField(decoration: InputDecoration(labelText: 'Email', hintText: 'example@example.com')),
TextField(decoration: InputDecoration(labelText: '住所', hintText: '〒000-0000 市区町村名・番地')),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')),
ElevatedButton(
onPressed: () async {
Navigator.pop(ctx);
_showSnackBar(context, '顧客データを保存します...');
},
child: const Text('保存'),
),
],
),
);
}
void _showMoreOptions(BuildContext context, Customer customer) {
showModalBottomSheet(
context: context,
builder: (ctx) => SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${customer.name}』のオプション機能', style: Theme.of(context).textTheme.titleLarge),
ListTile(leading: Icon(Icons.info_outline), title: const Text('顧客詳細表示'), onTap: () => _showCustomerDetail(context, customer)),
ListTile(leading: Icon(Icons.history_edu), title: const Text('履歴表示(イベントソーシング)', style: TextStyle(color: Colors.grey)), onTap: () => _showSnackBar(context, 'イベント履歴機能は後期開発')),
ListTile(leading: Icon(Icons.copy), title: const Text('QR コード発行(未実装)', style: TextStyle(color: Colors.grey)), onTap: () => _showSnackBar(context, 'QR コード機能は後期開発で')),
],
),
),
),
);
}
}