// 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 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; _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 _addCustomer(Customer customer) async { final db = await DatabaseHelper.instance.database; // 既存顧客リストを取得 final existingCustomers = (await db.query('customers')) as List>; 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 _editCustomer(Customer customer) async { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('編集機能:${customer.name}'), action: const SnackBarAction(label: 'キャンセル', onPressed: () {}), ), ); } 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.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 _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), ), ); } }