// Version: 1.0 - 担当者マスタ画面(リッチリスト実装) // ※ EmployeeEditDialog を使用した完全機能版 import 'package:flutter/material.dart'; import '../models/employee.dart'; import '../widgets/employee_edit_dialog.dart'; /// 担当者マスタ管理画面(リッチリスト) class EmployeeMasterScreen extends StatefulWidget { const EmployeeMasterScreen({super.key}); @override State createState() => _EmployeeMasterScreenState(); } class _EmployeeMasterScreenState extends State { List _employees = []; String _searchKeyword = ''; // セクション定義 const static List _sections = [ '営業部', '総務部', '経理部', '開発部', '製造部', '品質管理', 'HR', '法務', '物流部', 'IT' ]; @override void initState() { super.initState(); _loadEmployees(); } /// 従業員データをロード(サンプルデータ) Future _loadEmployees() async { setState(() => _loading = true); try { // リッチなサンプルデータ(8 件) final demoData = [ Employee(id: 1, name: '山田太郎', email: 'tanaka@company.com', tel: '03-5234-5678', department: '営業部', role: '営業部長'), Employee(id: 2, name: '田中花子', email: 'hanako@company.com', tel: '03-5345-6789', department: '営業部', role: '営業担当'), Employee(id: 3, name: '鈴木一郎', email: 'suzuki@company.com', tel: '03-5456-7890', department: '総務部', role: '総務主任'), Employee(id: 4, name: '高橋美咲', email: 'misaki@company.com', tel: '03-5567-8901', department: '経理部', role: '経理担当者'), Employee(id: 5, name: '伊藤健太', email: 'kenta@company.com', tel: '03-5678-9012', department: '開発部', role: '開発リーダー'), Employee(id: 6, name: '渡辺愛', email: 'mana@company.com', tel: '03-5789-0123', department: '製造部', role: '品質管理担当'), Employee(id: 7, name: '中村誠', email: 'makoto@company.com', tel: '03-5890-1234', department: '開発部', role: 'ソフトウェアエンジニア'), Employee(id: 8, name: '小林裕子', email: 'yuko@company.com', tel: '03-5901-2345', department: 'HR', role: '人事担当者'), ]; if (mounted) { setState(() => _employees = demoData); } } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red), ); } finally { setState(() => _loading = false); } } /// 検索された従業員リストを計算 List get _filteredEmployees { if (_searchKeyword.isEmpty) return _employees; return _employees.where((e) => e.name.toLowerCase().contains(_searchKeyword.toLowerCase()) || (e.department.isNotEmpty && e.department.toLowerCase().contains(_searchKeyword.toLowerCase())) || (e.role.isNotEmpty && e.role.toLowerCase().contains(_searchKeyword.toLowerCase())) ).toList(); } /// 新規従業員追加(ダイアログ表示) Future _addEmployee() async { final edited = await EmployeeEditDialog.show( context: context, title: '担当者登録', initialData: null, onSave: (employee) => setState(() => _employees.insert(0, employee)), ); if (edited != null && mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('担当者 "${edited.name}" を登録しました'), backgroundColor: Colors.green), ); } } /// 従業員編集(ダイアログ表示) Future _editEmployee(Employee employee) async { final edited = await EmployeeEditDialog.show( context: context, title: '担当者情報編集', initialData: employee, onSave: (updated) => setState(() { _employees = _employees.map((e) => e.id == updated.id ? updated : e).toList(); }), ); if (edited != null && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('担当者情報を更新しました'), backgroundColor: Colors.green), ); } } /// 従業員削除確認ダイアログ Future _deleteEmployee(Employee employee) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('担当者削除'), content: Text('本当に "${employee.name}" を削除しますか?'), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), style: ElevatedButton.styleFrom(backgroundColor: Colors.red.shade100), child: const Text('削除', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed == true && mounted) { setState(() => _employees.removeWhere((e) => e.id == employee.id)); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('担当者を削除しました'), backgroundColor: Colors.green), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('/M5. 担当者マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees), IconButton( icon: const Icon(Icons.add_circle_outline), onPressed: _addEmployee, tooltip: '新規登録', ), ], ), body: Column( children: [ // 検索バー Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.grey.shade50, border: Border( bottom: BorderSide(color: Colors.grey.shade200), ), ), child: Row( children: [ Icon(Icons.search, size: 18, color: Colors.grey.shade600), const SizedBox(width: 8), Expanded( child: TextField( decoration: InputDecoration( hintText: '担当者名・部署で検索...', hintStyle: TextStyle(color: Colors.grey.shade400), border: InputBorder.none, contentPadding: EdgeInsets.zero, ), onChanged: (value) => setState(() => _searchKeyword = value), style: const TextStyle(fontSize: 14), ), ), ], ), ), // リッチリストエリア Expanded( child: _loading ? _buildLoadingIndicator() : _filteredEmployees.isEmpty ? _buildEmptyState() : _buildRichList(), ), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: _addEmployee, icon: const Icon(Icons.person_add), label: const Text('新規登録'), ), ); } /// ローディングインジケーター Widget _buildLoadingIndicator() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 48, height: 48, child: CircularProgressIndicator(), ), const SizedBox(height: 16), const Text('担当者情報を取得中...', style: TextStyle(fontSize: 14)), ], ), ); } /// エンプティ状態表示 Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.people_outline, size: 80, color: Colors.grey.shade300), const SizedBox(height: 16), const Text('担当者データがありません', style: TextStyle(fontSize: 18)), const SizedBox(height: 24), OutlinedButton.icon( onPressed: _addEmployee, icon: const Icon(Icons.person_add), label: const Text('新規登録'), ), ], ), ), ); } /// リッチリストビルダー Widget _buildRichList() { return ListView.separated( padding: EdgeInsets.zero, itemCount: _filteredEmployees.length, separatorBuilder: (context, index) => Container(height: 1), itemBuilder: (context, index) { final employee = _filteredEmployees[index]; return _buildEmployeeCard(employee); }, ); } /// リッチな従業員カードビルダー Widget _buildEmployeeCard(Employee employee) { return Container( margin: const EdgeInsets.all(2), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( children: [ // アイコンエリア Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.purple.shade50, borderRadius: BorderRadius.circular(10), ), child: Icon( _getDepartmentIcon(employee.department), size: 24, color: Colors.purple.shade700, ), ), const SizedBox(width: 12), // データエリア Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( employee.name ?? '未入力', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), if (_getDepartmentIcon(employee.department) != Icons.business_rounded) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(6), ), child: Text( employee.department.isEmpty ? '未入力' : employee.department, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500), ), ), ], ], ), const SizedBox(height: 4), Row( children: [ if (employee.role.isNotEmpty) Expanded( child: Text( employee.role, style: TextStyle(fontSize: 12, color: Colors.grey.shade600), overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 4), if (employee.tel.isNotEmpty) Icon(Icons.phone, size: 14, color: Colors.grey.shade500), ], ), ], ), ), // 操作ボタンエリア Padding( padding: const EdgeInsets.only(left: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit, size: 18), onPressed: () => _editEmployee(employee), tooltip: '編集', padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), SizedBox(width: 2), IconButton( icon: const Icon(Icons.delete_outline, size: 18), onPressed: () => _deleteEmployee(employee), tooltip: '削除', padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ], ), ); } /// 部署アイコン取得(簡易版) IconData _getDepartmentIcon(String department) { if (department.contains('営業')) return Icons.work_outline; if (department.contains('総務')) return Icons.settings_applications_outlined; if (department.contains('経理')) return Icons.attach_money_outlined; if (department.contains('開発')) return Icons.code_outlined; if (department.contains('製造')) return Icons.construction_outlined; if (department.contains('HR')) return Icons.groups_outlined; if (department.contains('物流')) return Icons.local_shipping_outlined; if (department.contains('IT')) return Icons.cloud_outlined; return Icons.business_rounded; } }