h-1.flutter.4/@workspace/lib/screens/master/employee_master_screen.dart

368 lines
No EOL
13 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}
}