- widgets ディレクトリに MasterTextField, MasterNumberField, MasterDropdownField, MasterTextArea, MasterCheckBox を作成 - 各マスタ画面(product, customer, employee, supplier, warehouse)で統一ウィジェット化 - pubspec.yaml: flutter_form_builder の依存を整理(Flutter の標準機能で対応可能に)
327 lines
No EOL
13 KiB
Dart
327 lines
No EOL
13 KiB
Dart
// 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 コード機能は後期開発で')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |