368 lines
No EOL
13 KiB
Dart
368 lines
No EOL
13 KiB
Dart
// 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<EmployeeMasterScreen> createState() => _EmployeeMasterScreenState();
|
||
}
|
||
|
||
class _EmployeeMasterScreenState extends State<EmployeeMasterScreen> {
|
||
List<Employee> _employees = [];
|
||
String _searchKeyword = '';
|
||
|
||
// セクション定義
|
||
const static List<String> _sections = [
|
||
'営業部', '総務部', '経理部', '開発部', '製造部',
|
||
'品質管理', 'HR', '法務', '物流部', 'IT'
|
||
];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadEmployees();
|
||
}
|
||
|
||
/// 従業員データをロード(サンプルデータ)
|
||
Future<void> _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<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 {
|
||
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<void> _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<void> _deleteEmployee(Employee employee) async {
|
||
final confirmed = await showDialog<bool>(
|
||
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;
|
||
}
|
||
} |