From d036f43e2f60d78fdc9b32f6426f3213a0b612c4 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Mar 2026 20:48:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=85=E5=BD=93=E3=83=9E=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=AA=E3=83=83=E3=83=81=E5=8C=96=EF=BC=88M5=20?= =?UTF-8?q?=E6=8B=85=E5=BD=93=E3=83=9E=E3=82=B9=E3=82=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- @workspace/docs/short_term_plan.md | 70 ++++ .../master/employee_master_screen.dart | 366 +++++++++++++----- .../lib/widgets/employee_edit_dialog.dart | 365 ++++++++++------- 3 files changed, 570 insertions(+), 231 deletions(-) create mode 100644 @workspace/docs/short_term_plan.md diff --git a/@workspace/docs/short_term_plan.md b/@workspace/docs/short_term_plan.md new file mode 100644 index 0000000..9e35730 --- /dev/null +++ b/@workspace/docs/short_term_plan.md @@ -0,0 +1,70 @@ +# 少プロジェクト計画書 - 担当マスタ機能リッチ化 + +## プロジェクト概要 + +| 項目 | 内容 | +|------|------| +| **プロジェクト名** | 担当マスタ(M5)リッチ化改修 | +| **実装範囲** | Employee モデル・編集ダイアログ・画面の全面刷新 | +| **実装目標** | リッチなフォームコントロールによるユーザー体験向上 | + +## 実装方針 + +### フェーズ 1: エンティティモデル定義(完了) +- [x] `lib/models/employee.dart` - Employee モデル +- [ ] `lib/models/sample_employee.dart` - サンプルデータ + +### フェーズ 2: リッチ編集ダイアログ実装 +- [ ] **リッチな入力フィールド** + - アイコン付き TextField + - ラベル付きセクション分け + - ヒント表示機能 + +- [ ] **フォームデザイン** + - カード型レイアウト + - セクションヘッダー(基本情報・部署情報) + - 適切な余白配置 + +- [ ] **ダイアログ UI** + - テーマカラー適用 + - アクションボタンの Flex 配置 + - キャンセル/保存ボタンの明確な区別 + +### フェーズ 3: リッチマスタ画面実装 +- [ ] **リスト表示** + - データグリッド型レイアウト(または Cards) + - アイコン付き操作ボタン + - スwipe-to-action機能検討 + +- [ ] **検索/フィルタリング** + - 複数フィールド同時検索 + - ショートカットキーサポート + +- [ ] **サンプルデータ充実** + - 5~10 件のテストデータ + - 多様な部署・役職設定 + +### フェーズ 4: テストとデプロイ +- [ ] エミュレータでの動作確認 +- [ ] ビルド済み APK の生成 +- [ ] データベース永続化の検証 + +## 実装スケジュール + +| タスク | スコープ | 優先度 | +|--------|----------|--------| +| リッチ編集ダイアログ | high | P1 | +| リッチマスタ画面 | high | P1 | +| サンプルデータ拡張 | medium | P2 | +| ドキュメント更新 | low | P3 | + +## 技術的考慮事項 + +- **Material Design 3**: テーマカラーの適切な使用 +- **アクセシビリティ**: 適切なタッチターゲットサイズ +- **パフォーマンス**: リスト描画最適化(ListView.builder) +- **永続化**: SQLite データベースへの対応準備 + +--- + +*Version: 1.0 | Last Updated: 2026/3/11* \ No newline at end of file diff --git a/@workspace/lib/screens/master/employee_master_screen.dart b/@workspace/lib/screens/master/employee_master_screen.dart index d7a50d2..188c0e4 100644 --- a/@workspace/lib/screens/master/employee_master_screen.dart +++ b/@workspace/lib/screens/master/employee_master_screen.dart @@ -1,11 +1,11 @@ -// Version: 2.0 - 担当者マスタ画面(リッチ編集ダイアログ統合) -// ※ EmployeeEditDialog を使用した簡易実装 +// 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}); @@ -15,12 +15,13 @@ class EmployeeMasterScreen extends StatefulWidget { class _EmployeeMasterScreenState extends State { List _employees = []; - bool _loading = true; + String _searchKeyword = ''; - // 検索キーワード - String get _filteredEmployees => _searchKeyword.isEmpty ? _employees : - _employees.where((e) => e.name.toLowerCase().contains(_searchKeyword.toLowerCase()) || - (e.department.isNotEmpty && e.department.toLowerCase().contains(_searchKeyword.toLowerCase()))).toList(); + // セクション定義 + const static List _sections = [ + '営業部', '総務部', '経理部', '開発部', '製造部', + '品質管理', 'HR', '法務', '物流部', 'IT' + ]; @override void initState() { @@ -28,17 +29,25 @@ class _EmployeeMasterScreenState extends State { _loadEmployees(); } - /// 従業員データをロード(デモデータ) + /// 従業員データをロード(サンプルデータ) Future _loadEmployees() async { setState(() => _loading = true); try { - // サンプルデータを初期化 + // リッチなサンプルデータ(8 件) final demoData = [ - Employee(id: 1, name: '山田太郎', email: 'tanaka@company.com', tel: '03-1234-5678', department: '営業部', role: '営業担当'), - Employee(id: 2, name: '田中花子', email: 'tanaka@company.com', tel: '03-2345-6789', department: '総務部', role: '総務担当'), - Employee(id: 3, name: '鈴木一郎', email: 'suzuki@company.com', tel: '03-3456-7890', department: '経理部', role: '経理担当'), + 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: '人事担当者'), ]; - setState(() => _employees = demoData); + + if (mounted) { + setState(() => _employees = demoData); + } } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red), @@ -48,68 +57,74 @@ class _EmployeeMasterScreenState extends State { } } - /// 新規従業員追加 + /// 検索された従業員リストを計算 + 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 showDialog( + final edited = await EmployeeEditDialog.show( context: context, - builder: (ctx) => EmployeeEditDialog( - title: '担当者登録', - initialData: null, - ), + title: '担当者登録', + initialData: null, + onSave: (employee) => setState(() => _employees.insert(0, employee)), ); if (edited != null && mounted) { - setState(() => _employees.add(edited)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('担当者登録完了'), backgroundColor: Colors.green), + SnackBar(content: Text('担当者 "${edited.name}" を登録しました'), backgroundColor: Colors.green), ); } } - /// 従業員編集 + /// 従業員編集(ダイアログ表示) Future _editEmployee(Employee employee) async { - final edited = await showDialog( + final edited = await EmployeeEditDialog.show( context: context, - builder: (ctx) => EmployeeEditDialog( - title: '担当者編集', - initialData: employee, - ), + title: '担当者情報編集', + initialData: employee, + onSave: (updated) => setState(() { + _employees = _employees.map((e) => e.id == updated.id ? updated : e).toList(); + }), ); if (edited != null && mounted) { - setState(() { - _employees = _employees.map((e) => e.id == edited.id ? edited : e).toList(); - }); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('担当者更新完了'), backgroundColor: Colors.green), + 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('この担当者を実際に削除しますか?'), + 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), - child: const Text('削除'), + 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); - }); + setState(() => _employees.removeWhere((e) => e.id == employee.id)); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('担当者削除完了'), backgroundColor: Colors.green), + const SnackBar(content: Text('担当者を削除しました'), backgroundColor: Colors.green), ); } } @@ -121,78 +136,233 @@ class _EmployeeMasterScreenState extends State { title: const Text('/M5. 担当者マスタ'), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees), - IconButton(icon: const Icon(Icons.add), onPressed: _addEmployee), + IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: _addEmployee, + tooltip: '新規登録', + ), ], ), body: Column( children: [ // 検索バー - Padding( - padding: const EdgeInsets.all(8.0), - child: TextField( - decoration: InputDecoration( - hintText: '担当者名で検索...', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + bottom: BorderSide(color: Colors.grey.shade200), ), - onChanged: (value) => setState(() => _searchKeyword = value), + ), + 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 ? const Center(child: CircularProgressIndicator()) : - _filteredEmployees.isEmpty ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.person_outline, size: 64, color: Colors.grey[300]), - SizedBox(height: 16), - Text('担当者データがありません', style: TextStyle(color: Colors.grey)), - SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _addEmployee, - icon: const Icon(Icons.add), - label: const Text('新規登録'), - ), - ], + 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), ), - ) : ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: _filteredEmployees.length, - itemBuilder: (context, index) { - final employee = _filteredEmployees[index]; - return Card( - margin: EdgeInsets.zero, - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.purple.shade100, - child: Text('${employee.department.substring(0, 1)}', style: const TextStyle(fontWeight: FontWeight.bold)), - ), - title: Text(employee.name ?? '未入力', style: const TextStyle(fontWeight: FontWeight.w500)), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (employee.department.isNotEmpty) Text('部署:${employee.department}', style: const TextStyle(fontSize: 12)), - if (employee.role.isNotEmpty) Text('役職:${employee.role}', style: const TextStyle(fontSize: 12)), - if (employee.tel.isNotEmpty) Text('TEL: ${employee.tel}', style: const TextStyle(fontSize: 10, color: Colors.grey)), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton(icon: const Icon(Icons.edit), onPressed: () => _editEmployee(employee)), - IconButton(icon: const Icon(Icons.delete_outline), onPressed: () => _deleteEmployee(employee)), - ], - ), + 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; + } } \ No newline at end of file diff --git a/@workspace/lib/widgets/employee_edit_dialog.dart b/@workspace/lib/widgets/employee_edit_dialog.dart index c703639..2552a79 100644 --- a/@workspace/lib/widgets/employee_edit_dialog.dart +++ b/@workspace/lib/widgets/employee_edit_dialog.dart @@ -1,6 +1,4 @@ -// 従業員編集ダイアログ(リッチ版) -// ※ Employee モデルに特化した編集用ダイアログ - +// Version: 1.0 - 担当従業員編集ダイアログ(リッチ実装) import 'package:flutter/material.dart'; import '../models/employee.dart'; @@ -8,13 +6,15 @@ import '../models/employee.dart'; class EmployeeEditDialog extends StatefulWidget { final String title; final Employee? initialData; // null = 新規作成 - final void Function(Employee) onSave; // 保存コールバック + + /// 保存時のコールバック(Employee のデータを返す) + final void Function(Employee)? onSave; const EmployeeEditDialog({ super.key, required this.title, this.initialData, - required this.onSave, + this.onSave, }); @override @@ -32,11 +32,19 @@ class _EmployeeEditDialogState extends State { void initState() { super.initState(); final data = widget.initialData; - nameController = TextEditingController(text: data?.name ?? ''); - emailController = TextEditingController(text: data?.email ?? ''); - telController = TextEditingController(text: data?.tel ?? ''); - departmentController = TextEditingController(text: data?.department ?? ''); - roleController = TextEditingController(text: data?.role ?? ''); + if (data == null) { + nameController = TextEditingController(text: ''); + emailController = TextEditingController(text: ''); + telController = TextEditingController(text: ''); + departmentController = TextEditingController(text: ''); + roleController = TextEditingController(text: ''); + } else { + nameController = TextEditingController(text: data.name); + emailController = TextEditingController(text: data.email); + telController = TextEditingController(text: data.tel); + departmentController = TextEditingController(text: data.department); + roleController = TextEditingController(text: data.role); + } } @override @@ -50,10 +58,10 @@ class _EmployeeEditDialogState extends State { } /// リッチな入力フィールドビルダー(共通) - Widget _buildRichTextField( - String label, - TextEditingController controller, { - TextInputType? keyboard, + Widget _buildRichTextField({ + required String label, + required TextEditingController controller, { + TextInputType? keyboardType, IconData? icon, String hint = '', }) { @@ -64,25 +72,38 @@ class _EmployeeEditDialogState extends State { children: [ Text( label, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.grey.shade700), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 13, + color: Colors.grey.shade700, + ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( + color: Theme.of(context).cardColor.withOpacity(0.3), border: Border.all(color: Theme.of(context).dividerColor), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), ), - child: TextField( - controller: controller, - keyboardType: keyboard, - style: const TextStyle(fontSize: 14), - decoration: InputDecoration( - hintText: hint.isEmpty ? null : hint, - prefixIcon: Icon(icon, size: 16, color: Theme.of(context).primaryColor), - border: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), + child: Row( + children: [ + if (icon != null) + Icon(icon, size: 16, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded( + child: TextField( + controller: controller, + keyboardType: keyboardType, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: hint.isEmpty ? null : hint, + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ], ), ), ], @@ -90,10 +111,30 @@ class _EmployeeEditDialogState extends State { ); } + /// ダイアログ表示用 static メソッド + static Future show({ + required BuildContext context, + required String title, + required Employee? initialData, + required void Function(Employee) onSave, + }) async { + final dialog = EmployeeEditDialog( + title: title, + initialData: initialData, + ); + + // ダイアログをビルドして表示 + showDialog( + context: context, + builder: (ctx) => dialog._build(context), + ); + } + @override Widget build(BuildContext context) { return Dialog( backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( constraints: const BoxConstraints(maxWidth: 420), padding: const EdgeInsets.all(16), @@ -102,151 +143,204 @@ class _EmployeeEditDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // タイトル + // タイトルバー Row( children: [ - Icon(Icons.person, size: 20, color: Theme.of(context).primaryColor), - const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(Icons.person, size: 24, color: Theme.of(context).primaryColor), + ), + const SizedBox(width: 12), Expanded(child: Text( widget.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), )), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + widget.initialData == null ? '新規' : '編集', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + ), + ), IconButton( icon: Icon(Icons.close, color: Colors.grey), onPressed: () => Navigator.pop(context), ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), // ヒントテキスト Center( child: Text( - '新規作成の場合は「空白」から入力して OK を押してください', - style: TextStyle(fontSize: 12, color: Colors.grey.shade500, fontStyle: FontStyle.italic), + widget.initialData == null + ? '担当者情報を登録してください' + : '担当者情報を更新してください', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + fontStyle: FontStyle.italic, + ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 16), - // リッチな編集フォーム + // ヒーダーセクション Container( + padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).cardColor, + color: Theme.of(context).primaryColor.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: Theme.of(context).dividerColor), ), - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - // 基本情報セクション - Text( - '■ 基本情報', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + Icon(Icons.business, size: 20, color: Theme.of(context).primaryColor), + const SizedBox(width: 8), + Expanded( + child: Text( + '担当者情報を入力してください', + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14), ), ), - const SizedBox(height: 8), - - // 名前フィールド - _buildRichTextField( - '氏名 *', - nameController, - keyboard: TextInputType.name, - icon: Icons.person, - hint: e.g., '山田太郎', - ), - - // メールアドレスフィールド - _buildRichTextField( - 'E メール *', - emailController, - keyboard: TextInputType.emailAddress, - icon: Icons.email, - hint: 'example@company.com', - ), - - // 電話番号フィールド - _buildRichTextField( - '電話番号 *', - telController, - keyboard: TextInputType.phone, - icon: Icons.phone, - hint: '03-1234-5678', - ), - - const Divider(), - - // 部署情報セクション - Text( - '■ 部署・役職', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - ), - ), - const SizedBox(height: 8), - - // 部門フィールド - _buildRichTextField( - '部署 *', - departmentController, - keyboard: TextInputType.text, - icon: Icons.business, - hint: '営業部', - ), - - // 役職フィールド - _buildRichTextField( - '役職 *', - roleController, - keyboard: TextInputType.text, - icon: Icons.badge, - hint: '営業担当', - ), ], ), ), + const SizedBox(height: 16), + // リッチな編集フォーム + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ──────────────── 基本情報 ──────────────── + Text( + '■ 基本情報', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 12), + + // 名前フィールド + _buildRichTextField( + label: '氏名 *', + controller: nameController, + keyboardType: TextInputType.name, + icon: Icons.person, + hint: '山田太郎', + ), + + // メールアドレスフィールド + _buildRichTextField( + label: 'E メール *', + controller: emailController, + keyboardType: TextInputType.emailAddress, + icon: Icons.email, + hint: 'tanaka@company.com', + ), + + // 電話番号フィールド + _buildRichTextField( + label: '電話番号 *', + controller: telController, + keyboardType: TextInputType.phone, + icon: Icons.phone, + hint: '03-1234-5678', + ), + + const Divider(height: 24), + + // ──────────────── 部署情報 ──────────────── + Text( + '■ 部署・役職', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Theme.of(context).primaryColor, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 12), + + // 部門フィールド + _buildRichTextField( + label: '部署 *', + controller: departmentController, + keyboardType: TextInputType.text, + icon: Icons.business, + hint: '営業部', + ), + + // 役職フィールド + _buildRichTextField( + label: '役職 *', + controller: roleController, + keyboardType: TextInputType.text, + icon: Icons.badge, + hint: '営業担当', + ), + ], + ), const SizedBox(height: 16), // アクションボタン(Flex で配置) - Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => Navigator.pop(context), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.close, size: 18), + label: const Text(' キャンセル ', style: TextStyle(fontSize: 14)), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: Colors.grey.shade300), + ), ), - child: Text(' キャンセル ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), ), - ), - const SizedBox(width: 8), - Expanded( - flex: 3, // より広いボタン - child: ElevatedButton( - onPressed: () { - final employee = Employee( - id: widget.initialData?.id ?? -1, - name: nameController.text.isEmpty ? widget.initialData?.name ?? '未入力' : nameController.text, - email: emailController.text.isEmpty ? widget.initialData?.email ?? '未入力' : emailController.text, - tel: telController.text.isEmpty ? widget.initialData?.tel ?? '未入力' : telController.text, - department: departmentController.text.isEmpty ? widget.initialData?.department ?? '未入力' : departmentController.text, - role: roleController.text.isEmpty ? widget.initialData?.role ?? '未入力' : roleController.text, - ); - widget.onSave(employee); - }, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 14), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + if (widget.onSave != null) { + final employee = Employee( + id: widget.initialData?.id ?? -1, + name: nameController.text.isEmpty ? widget.initialData?.name ?? '未入力' : nameController.text, + email: emailController.text.isEmpty ? widget.initialData?.email ?? '未入力' : emailController.text, + tel: telController.text.isEmpty ? widget.initialData?.tel ?? '未入力' : telController.text, + department: departmentController.text.isEmpty ? widget.initialData?.department ?? '未入力' : departmentController.text, + role: roleController.text.isEmpty ? widget.initialData?.role ?? '未入力' : roleController.text, + ); + widget.onSave(employee); + } + }, + icon: Icon(Icons.save, size: 18), + label: const Text(' 保存 ', style: TextStyle(fontSize: 14)), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + ), ), - child: Text(' 保存 ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), ), - ), - ], + ], + ), ), ], ), @@ -254,4 +348,9 @@ class _EmployeeEditDialogState extends State { ), ); } + + /// 公開用ビルドメソッド(static メソッドから使用) + Widget _build(BuildContext context) { + return this; + } } \ No newline at end of file