- 機能要件・非機能要件定義 - 短期長期プロジェクト計画の策定 docs: UI ライティングリファクタリング - 編集 SnackBar から Cancel ボタン削除 - タイル表示からプレースホルダメッセージへ</new_task>chore: README のドキュメント活用方法追記</new_task>
376 lines
No EOL
14 KiB
Dart
376 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}'),
|
|
),
|
|
);
|
|
}
|
|
|
|
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),
|
|
),
|
|
);
|
|
}
|
|
} |