feat: 担当マスタリッチ化(M5 担当マスタ)

This commit is contained in:
joe 2026-03-11 20:48:01 +09:00
parent bd1e2be03e
commit d036f43e2f
3 changed files with 570 additions and 231 deletions

View file

@ -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*

View file

@ -1,11 +1,11 @@
// Version: 2.0 - // Version: 1.0 -
// EmployeeEditDialog 使 // EmployeeEditDialog 使
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/employee.dart'; import '../models/employee.dart';
import '../widgets/employee_edit_dialog.dart'; import '../widgets/employee_edit_dialog.dart';
/// ///
class EmployeeMasterScreen extends StatefulWidget { class EmployeeMasterScreen extends StatefulWidget {
const EmployeeMasterScreen({super.key}); const EmployeeMasterScreen({super.key});
@ -15,12 +15,13 @@ class EmployeeMasterScreen extends StatefulWidget {
class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> { class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> {
List<Employee> _employees = []; List<Employee> _employees = [];
bool _loading = true; String _searchKeyword = '';
// //
String get _filteredEmployees => _searchKeyword.isEmpty ? _employees : const static List<String> _sections = [
_employees.where((e) => e.name.toLowerCase().contains(_searchKeyword.toLowerCase()) || '営業部', '総務部', '経理部', '開発部', '製造部',
(e.department.isNotEmpty && e.department.toLowerCase().contains(_searchKeyword.toLowerCase()))).toList(); '品質管理', 'HR', '法務', '物流部', 'IT'
];
@override @override
void initState() { void initState() {
@ -28,17 +29,25 @@ class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> {
_loadEmployees(); _loadEmployees();
} }
/// ///
Future<void> _loadEmployees() async { Future<void> _loadEmployees() async {
setState(() => _loading = true); setState(() => _loading = true);
try { try {
// // 8
final demoData = [ final demoData = [
Employee(id: 1, name: '山田太郎', email: 'tanaka@company.com', tel: '03-1234-5678', department: '営業部', role: '営業担当'), Employee(id: 1, name: '山田太郎', email: 'tanaka@company.com', tel: '03-5234-5678', department: '営業部', role: '営業部長'),
Employee(id: 2, name: '田中花子', email: 'tanaka@company.com', tel: '03-2345-6789', 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-3456-7890', 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) { } catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar( if (mounted) ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red), SnackBar(content: Text('読み込みエラー:$e'), backgroundColor: Colors.red),
@ -48,68 +57,74 @@ class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> {
} }
} }
/// ///
List<Employee> 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<void> _addEmployee() async { Future<void> _addEmployee() async {
final edited = await showDialog<Employee>( final edited = await EmployeeEditDialog.show(
context: context, context: context,
builder: (ctx) => EmployeeEditDialog( title: '担当者登録',
title: '担当者登録', initialData: null,
initialData: null, onSave: (employee) => setState(() => _employees.insert(0, employee)),
),
); );
if (edited != null && mounted) { if (edited != null && mounted) {
setState(() => _employees.add(edited));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('担当者登録完了'), backgroundColor: Colors.green), SnackBar(content: Text('担当者 "${edited.name}" を登録しました'), backgroundColor: Colors.green),
); );
} }
} }
/// ///
Future<void> _editEmployee(Employee employee) async { Future<void> _editEmployee(Employee employee) async {
final edited = await showDialog<Employee>( final edited = await EmployeeEditDialog.show(
context: context, context: context,
builder: (ctx) => EmployeeEditDialog( title: '担当者情報編集',
title: '担当者編集', initialData: employee,
initialData: employee, onSave: (updated) => setState(() {
), _employees = _employees.map((e) => e.id == updated.id ? updated : e).toList();
}),
); );
if (edited != null && mounted) { if (edited != null && mounted) {
setState(() {
_employees = _employees.map((e) => e.id == edited.id ? edited : e).toList();
});
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('担当者更新完了'), backgroundColor: Colors.green), const SnackBar(content: Text('担当者情報を更新しました'), backgroundColor: Colors.green),
); );
} }
} }
/// ///
Future<void> _deleteEmployee(Employee employee) async { Future<void> _deleteEmployee(Employee employee) async {
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('担当者削除'), title: const Text('担当者削除'),
content: Text('この担当者を実際に削除しますか?'), content: Text('本当に "${employee.name}" を削除しますか?'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')), TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('キャンセル')),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(ctx, true), onPressed: () => Navigator.pop(ctx, true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: Colors.red.shade100),
child: const Text('削除'), child: const Text('削除', style: TextStyle(color: Colors.red)),
), ),
], ],
), ),
); );
if (confirmed == true && mounted) { if (confirmed == true && mounted) {
setState(() { setState(() => _employees.removeWhere((e) => e.id == employee.id));
_employees.removeWhere((e) => e.id == employee.id);
});
ScaffoldMessenger.of(context).showSnackBar( 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<EmployeeMasterScreen> {
title: const Text('/M5. 担当者マスタ'), title: const Text('/M5. 担当者マスタ'),
actions: [ actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _loadEmployees), 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( body: Column(
children: [ children: [
// //
Padding( Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: TextField( decoration: BoxDecoration(
decoration: InputDecoration( color: Colors.grey.shade50,
hintText: '担当者名で検索...', border: Border(
prefixIcon: const Icon(Icons.search), bottom: BorderSide(color: Colors.grey.shade200),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
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( Expanded(
child: _loading ? const Center(child: CircularProgressIndicator()) : child: _loading ? _buildLoadingIndicator() :
_filteredEmployees.isEmpty ? Center( _filteredEmployees.isEmpty ? _buildEmptyState() :
child: Column( _buildRichList(),
mainAxisAlignment: MainAxisAlignment.center, ),
children: [ ],
Icon(Icons.person_outline, size: 64, color: Colors.grey[300]), ),
SizedBox(height: 16), floatingActionButton: FloatingActionButton.extended(
Text('担当者データがありません', style: TextStyle(color: Colors.grey)), onPressed: _addEmployee,
SizedBox(height: 16), icon: const Icon(Icons.person_add),
ElevatedButton.icon( label: const Text('新規登録'),
onPressed: _addEmployee, ),
icon: const Icon(Icons.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( if (_getDepartmentIcon(employee.department) != Icons.business_rounded) ...[
padding: const EdgeInsets.all(8), const SizedBox(width: 8),
itemCount: _filteredEmployees.length, Container(
itemBuilder: (context, index) { padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
final employee = _filteredEmployees[index]; decoration: BoxDecoration(
return Card( color: Colors.grey.shade100,
margin: EdgeInsets.zero, borderRadius: BorderRadius.circular(6),
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)),
],
),
), ),
); 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;
}
} }

View file

@ -1,6 +1,4 @@
// // Version: 1.0 -
// Employee
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/employee.dart'; import '../models/employee.dart';
@ -8,13 +6,15 @@ import '../models/employee.dart';
class EmployeeEditDialog extends StatefulWidget { class EmployeeEditDialog extends StatefulWidget {
final String title; final String title;
final Employee? initialData; // null = final Employee? initialData; // null =
final void Function(Employee) onSave; //
/// Employee
final void Function(Employee)? onSave;
const EmployeeEditDialog({ const EmployeeEditDialog({
super.key, super.key,
required this.title, required this.title,
this.initialData, this.initialData,
required this.onSave, this.onSave,
}); });
@override @override
@ -32,11 +32,19 @@ class _EmployeeEditDialogState extends State<EmployeeEditDialog> {
void initState() { void initState() {
super.initState(); super.initState();
final data = widget.initialData; final data = widget.initialData;
nameController = TextEditingController(text: data?.name ?? ''); if (data == null) {
emailController = TextEditingController(text: data?.email ?? ''); nameController = TextEditingController(text: '');
telController = TextEditingController(text: data?.tel ?? ''); emailController = TextEditingController(text: '');
departmentController = TextEditingController(text: data?.department ?? ''); telController = TextEditingController(text: '');
roleController = TextEditingController(text: data?.role ?? ''); 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 @override
@ -50,10 +58,10 @@ class _EmployeeEditDialogState extends State<EmployeeEditDialog> {
} }
/// ///
Widget _buildRichTextField( Widget _buildRichTextField({
String label, required String label,
TextEditingController controller, { required TextEditingController controller, {
TextInputType? keyboard, TextInputType? keyboardType,
IconData? icon, IconData? icon,
String hint = '', String hint = '',
}) { }) {
@ -64,25 +72,38 @@ class _EmployeeEditDialogState extends State<EmployeeEditDialog> {
children: [ children: [
Text( Text(
label, 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), const SizedBox(height: 4),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).cardColor.withOpacity(0.3),
border: Border.all(color: Theme.of(context).dividerColor), border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
), ),
child: TextField( child: Row(
controller: controller, children: [
keyboardType: keyboard, if (icon != null)
style: const TextStyle(fontSize: 14), Icon(icon, size: 16, color: Theme.of(context).primaryColor),
decoration: InputDecoration( const SizedBox(width: 8),
hintText: hint.isEmpty ? null : hint, Expanded(
prefixIcon: Icon(icon, size: 16, color: Theme.of(context).primaryColor), child: TextField(
border: InputBorder.none, controller: controller,
contentPadding: EdgeInsets.zero, 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<EmployeeEditDialog> {
); );
} }
/// static
static Future<Employee?> show({
required BuildContext context,
required String title,
required Employee? initialData,
required void Function(Employee) onSave,
}) async {
final dialog = EmployeeEditDialog(
title: title,
initialData: initialData,
);
//
showDialog<Employee>(
context: context,
builder: (ctx) => dialog._build(context),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Dialog( return Dialog(
backgroundColor: Colors.white, backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 420), constraints: const BoxConstraints(maxWidth: 420),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@ -102,151 +143,204 @@ class _EmployeeEditDialogState extends State<EmployeeEditDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// //
Row( Row(
children: [ children: [
Icon(Icons.person, size: 20, color: Theme.of(context).primaryColor), Container(
const SizedBox(width: 8), 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( Expanded(child: Text(
widget.title, widget.title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), 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( IconButton(
icon: Icon(Icons.close, color: Colors.grey), icon: Icon(Icons.close, color: Colors.grey),
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
), ),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 16),
// //
Center( Center(
child: Text( child: Text(
'新規作成の場合は「空白」から入力して OK を押してください', widget.initialData == null
style: TextStyle(fontSize: 12, color: Colors.grey.shade500, fontStyle: FontStyle.italic), ? '担当者情報を登録してください'
: '担当者情報を更新してください',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
fontStyle: FontStyle.italic,
),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 16),
// //
Container( Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).cardColor, color: Theme.of(context).primaryColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Theme.of(context).dividerColor), border: Border.all(color: Theme.of(context).dividerColor),
), ),
padding: const EdgeInsets.all(12), child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Icon(Icons.business, size: 20, color: Theme.of(context).primaryColor),
Text( const SizedBox(width: 8),
'■ 基本情報', Expanded(
style: TextStyle( child: Text(
fontSize: 14, '担当者情報を入力してください',
fontWeight: FontWeight.bold, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
color: Theme.of(context).primaryColor,
), ),
), ),
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), const SizedBox(height: 16),
// Flex // Flex
Row( Container(
children: [ padding: const EdgeInsets.all(12),
Expanded( decoration: BoxDecoration(
child: OutlinedButton( color: Colors.grey.shade50,
onPressed: () => Navigator.pop(context), borderRadius: BorderRadius.circular(12),
style: OutlinedButton.styleFrom( ),
padding: const EdgeInsets.symmetric(vertical: 14), 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),
const SizedBox(width: 8), Expanded(
Expanded( child: ElevatedButton.icon(
flex: 3, // onPressed: () {
child: ElevatedButton( if (widget.onSave != null) {
onPressed: () { final employee = Employee(
final employee = Employee( id: widget.initialData?.id ?? -1,
id: widget.initialData?.id ?? -1, name: nameController.text.isEmpty ? widget.initialData?.name ?? '未入力' : nameController.text,
name: nameController.text.isEmpty ? widget.initialData?.name ?? '未入力' : nameController.text, email: emailController.text.isEmpty ? widget.initialData?.email ?? '未入力' : emailController.text,
email: emailController.text.isEmpty ? widget.initialData?.email ?? '未入力' : emailController.text, tel: telController.text.isEmpty ? widget.initialData?.tel ?? '未入力' : telController.text,
tel: telController.text.isEmpty ? widget.initialData?.tel ?? '未入力' : telController.text, department: departmentController.text.isEmpty ? widget.initialData?.department ?? '未入力' : departmentController.text,
department: departmentController.text.isEmpty ? widget.initialData?.department ?? '未入力' : departmentController.text, role: roleController.text.isEmpty ? widget.initialData?.role ?? '未入力' : roleController.text,
role: roleController.text.isEmpty ? widget.initialData?.role ?? '未入力' : roleController.text, );
); widget.onSave(employee);
widget.onSave(employee); }
}, },
style: ElevatedButton.styleFrom( icon: Icon(Icons.save, size: 18),
padding: const EdgeInsets.symmetric(vertical: 14), 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<EmployeeEditDialog> {
), ),
); );
} }
/// static 使
Widget _build(BuildContext context) {
return this;
}
} }