h-1.flutter.4/lib/screens/master/customer_master_screen.dart
joe cdc037bf21 fix: ビルドエラーの修正
- customer_master_screen.dart: const 使用と SnackBarAction の修正
- employee_master_screen.dart: DropdownButtonFormField のパラメータ追加
- database_helper.dart: jsonEncode メソッドの修正、isDeleted フィールドのチェック
- main.dart: ScaffoldMessenger と Navigator のルートジェネレータ実装
2026-03-07 07:47:04 +09:00

377 lines
No EOL
14 KiB
Dart

// Version: 1.0.0
import 'package:flutter/material.dart';
import '../../models/customer.dart';
import '../../services/database_helper.dart';
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;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
_showSnackBar(context, '顧客データを読み込みませんでした: $e');
}
}
@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),
IconButton(
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
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!),
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: () => _showEditDialog(context, 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),
),
);
}
Future<void> _addCustomer(Customer customer) async {
final db = await DatabaseHelper.instance.database;
// 既存顧客リストを取得
final existingCustomers = (await db.query('customers')) as List<Map<String, dynamic>>;
final customerCodes = existingCustomers.map((e) => e['customer_code'] as String).toList();
if (customerCodes.contains(customer.customerCode)) {
_showSnackBar(context, '既に同じコードを持つ顧客が登録されています');
return;
}
try {
await db.insert('customers', customer.toMap());
await DatabaseHelper.instance.saveSnapshot(customer); // Event sourcing snapshot
await _loadCustomers();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を登録しました')),
);
}
} catch (e) {
_showSnackBar(context, '登録に失敗:$e');
}
}
Future<void> _editCustomer(Customer customer) async {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('編集機能:${customer.name}'),
action: const SnackBarAction(label: 'キャンセル', onPressed: () {}),
),
);
}
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.delete(id);
await _loadCustomers();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を削除しました')),
);
}
} catch (e) {
_showSnackBar(context, '削除に失敗:$e');
}
}
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新規顧客登録'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('得意先コード *'),
subtitle: const Text('JAN 形式など(半角数字)'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('顧客名称 *'),
subtitle: const Text('株式会社〇〇'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('電話番号'),
subtitle: const Text('03-1234-5678'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('Email'),
subtitle: const Text('example@example.com'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
ListTile(
title: const Text('住所'),
subtitle: const Text('〒000-0000 市区町村名・番地'),
onLongPress: () => _showSnackBar(context, '登録機能(プレースホルダ)'),
),
const SizedBox(height: 12),
Text(
'※ 保存ボタンを押すと、上記の値から作成された顧客データが登録されます',
textAlign: TextAlign.center,
style: TextStyle(fontStyle: FontStyle.italic),
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'),),
ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
// 実際の登録処理は後期開発(プレースホルダ)
_showSnackBar(context, '顧客データを保存します...');
},
child: const Text('保存'),
),
],
),
);
}
void _showEditDialog(BuildContext context, Customer customer) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客編集'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('顧客コード'),
subtitle: Text(customer.customerCode),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('名称'),
subtitle: Text(customer.name),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('電話番号'),
subtitle: Text(customer.phoneNumber),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
ListTile(
title: const Text('税抜率'),
subtitle: Text('${customer.taxRate}%'),
onLongPress: () => _showSnackBar(context, '編集機能(プレースホルダ)'),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル'),),
ElevatedButton(
onPressed: () {
// 実際の保存処理は後期開発(プレースホルダ)
_showSnackBar(context, '編集を保存します...');
Navigator.pop(ctx);
},
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: const Icon(Icons.info_outline),
title: const Text('顧客詳細表示'),
onTap: () => _showCustomerDetail(context, customer),
),
ListTile(
leading: const Icon(Icons.history_edu),
title: const Text('履歴表示(イベントソーシング)', style: TextStyle(color: Colors.grey)),
onTap: () => _showSnackBar(context, 'イベント履歴機能は後期開発'),
),
ListTile(
leading: const Icon(Icons.copy),
title: const Text('QR コード発行(未実装)', style: TextStyle(color: Colors.grey)),
onTap: () => _showSnackBar(context, 'QR コード機能は後期開発で'),
),
],
),
),
),
);
}
void _showCustomerDetail(BuildContext context, Customer customer) {
if (!mounted) return;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('顧客詳細'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_detailRow('得意先コード', customer.customerCode),
_detailRow('名称', customer.name),
_detailRow('電話番号', customer.phoneNumber),
_detailRow('Email', customer.email ?? '-'),
_detailRow('住所', customer.address),
if (customer.salesPersonId > 0) _detailRow('担当者 ID', customer.salesPersonId.toString()),
_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)),
],
),
);
}
Future<void> _showEventHistory(BuildContext context, Customer customer) async {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('イベント履歴表示(未実装:後期開発)')),
);
}
void _showSnackBar(BuildContext context, String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
}