// 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 createState() => _CustomerMasterScreenState(); } class _CustomerMasterScreenState extends State { final DatabaseHelper _db = DatabaseHelper.instance; List _customers = []; bool _isLoading = true; @override void initState() { super.initState(); _loadCustomers(); } Future _loadCustomers() async { try { final customers = await _db.getCustomers(); setState(() { _customers = customers ?? const []; _isLoading = false; }); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('顧客データを読み込みませんでした:$e'), backgroundColor: Colors.red), ); } } Future _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 _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 _deleteCustomer(int id) async { final confirmed = await showDialog( 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 _showEditDialog(BuildContext context, Customer customer) async { final edited = await 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: 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 _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('/M2. 得意先マスタ'), 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 コード機能は後期開発で')), ], ), ), ), ); } }