248 lines
No EOL
8.9 KiB
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')));
|
|
}
|
|
}
|
|
} |