h-1.flutter.4/@workspace/lib/screens/master/customer_master_screen.dart

248 lines
No EOL
8.9 KiB
Dart

// Version: 3.0 - シンプル得意先マスタ画面(簡素版)
// ※ MasterEditDialog を使用するため、独自の実装は不要です
import 'package:flutter/material.dart';
import '../../models/customer.dart';
import '../../services/database_helper.dart';
import 'master_edit_dialog.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();
if (mounted) 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() async {
final customer = await showDialog<Customer>(
context: context,
builder: (ctx) => MasterEditDialog(
title: '新規得意先登録',
onSave: (data) async {
if (mounted) {
setState(() => _customers.insert(0, data));
await _db.insertCustomer(data);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を登録しました'), backgroundColor: Colors.green),
);
_loadCustomers();
}
return true;
},
),
);
if (customer != null) _loadCustomers();
}
Future<void> _editCustomer(Customer customer) async {
final updated = await showDialog<Customer>(
context: context,
builder: (ctx) => MasterEditDialog(
title: '得意先編集',
initialData: customer,
showStatusFields: true,
onSave: (data) async {
if (mounted) {
await _db.updateCustomer(data);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を更新しました'), backgroundColor: Colors.green),
);
_loadCustomers();
}
return true;
},
),
);
if (updated != null) _loadCustomers();
}
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 _db.deleteCustomer(id);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('顧客を削除しました'), backgroundColor: Colors.green),
);
_loadCustomers();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('削除に失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('/M2. 得意先マスタ')),
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: _addCustomer,
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _customers.length,
itemBuilder: (context, index) {
final customer = _customers[index];
return Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 4,
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.isNotEmpty) Text('Email: ${customer.email}', style: const TextStyle(fontSize: 12)),
Text('登録日:${DateFormat('yyyy/MM/dd').format(customer.createdAt ?? DateTime.now())}'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(icon: const Icon(Icons.edit), onPressed: () => _editCustomer(customer)),
PopupMenuButton<String>(
onSelected: (value) => value == 'delete' ? _deleteCustomer(customer.id ?? 0) : null,
itemBuilder: (ctx) => [
PopupMenuItem(child: const Text('詳細'), onPressed: () => _showDetail(context, customer)),
PopupMenuItem(child: const Text('削除'), onPressed: () => _deleteCustomer(customer.id ?? 0)),
],
),
],
),
),
);
},
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('新規登録'),
onPressed: _addCustomer,
),
);
}
Future<void> _showDetail(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 ?? '-'),
_detailRow('Email', customer.email.isNotEmpty ? customer.email : '-'),
_detailRow('登録日', DateFormat('yyyy/MM/dd').format(customer.createdAt)),
],
),
),
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('閉じる'))],
),
);
}
Widget _detailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 80),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
if (value != '-') Text(value),
],
),
),
],
),
);
}
Future<void> _onCopyFromOtherMaster() async {
final selected = await showDialog<String?>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('他のマスタからコピー'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(leading: Icon(Icons.store, color: Colors.blue), title: const Text('仕入先マスタから'), onTap: () => Navigator.pop(ctx, 'supplier')),
ListTile(leading: Icon(Icons.inventory_2, color: Colors.orange), title: const Text('商品マスタから'), onTap: () => Navigator.pop(ctx, 'product')),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')),
],
),
);
if (selected != null && mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('コピー機能は後期開発:$selected')));
}
}
}