h-1.flutter.0/lib/screens/department_master_screen.dart
2026-03-04 14:55:40 +09:00

225 lines
8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/department_model.dart';
import '../services/department_repository.dart';
class DepartmentMasterScreen extends StatefulWidget {
const DepartmentMasterScreen({super.key});
@override
State<DepartmentMasterScreen> createState() => _DepartmentMasterScreenState();
}
class _DepartmentMasterScreenState extends State<DepartmentMasterScreen> {
final DepartmentRepository _repository = DepartmentRepository();
final Uuid _uuid = const Uuid();
bool _isLoading = true;
bool _includeInactive = true;
List<Department> _departments = const [];
@override
void initState() {
super.initState();
_loadDepartments();
}
Future<void> _loadDepartments() async {
setState(() => _isLoading = true);
final list = await _repository.fetchDepartments(includeInactive: _includeInactive);
if (!mounted) return;
setState(() {
_departments = list;
_isLoading = false;
});
}
Future<void> _openForm({Department? department}) async {
final result = await showDialog<Department>(
context: context,
builder: (ctx) => _DepartmentFormDialog(
department: department,
onSubmit: (data) => Navigator.of(ctx).pop(data),
),
);
if (result == null) return;
final saving = result.copyWith(id: result.id.isEmpty ? _uuid.v4() : result.id, updatedAt: DateTime.now());
await _repository.saveDepartment(saving);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('部門を保存しました')));
_loadDepartments();
}
Future<void> _deleteDepartment(Department department) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('部門を削除'),
content: Text('${department.name} を削除しますか?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('削除', style: TextStyle(color: Colors.red))),
],
),
);
if (confirmed != true) return;
await _repository.deleteDepartment(department.id);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('部門を削除しました')));
_loadDepartments();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: const BackButton(),
title: const Text('M4:部門マスター'),
actions: [
SwitchListTile.adaptive(
value: _includeInactive,
onChanged: (value) {
setState(() => _includeInactive = value);
_loadDepartments();
},
contentPadding: const EdgeInsets.only(right: 12),
title: const Text('無効を表示'),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => _openForm(),
child: const Icon(Icons.add),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _departments.isEmpty
? const _EmptyState(message: '部門が登録されていません')
: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _departments.length,
separatorBuilder: (context, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final department = _departments[index];
return Card(
child: ListTile(
title: Text(department.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text([
if (department.code?.isNotEmpty == true) 'コード: ${department.code}',
department.description ?? '',
department.isActive ? '稼働中' : '無効',
].where((v) => v.isNotEmpty).join('\n')),
trailing: PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'edit':
_openForm(department: department);
break;
case 'delete':
_deleteDepartment(department);
break;
}
},
itemBuilder: (context) => const [
PopupMenuItem(value: 'edit', child: Text('編集')),
PopupMenuItem(value: 'delete', child: Text('削除')),
],
),
),
);
},
),
);
}
}
class _DepartmentFormDialog extends StatefulWidget {
const _DepartmentFormDialog({required this.onSubmit, this.department});
final Department? department;
final ValueChanged<Department> onSubmit;
@override
State<_DepartmentFormDialog> createState() => _DepartmentFormDialogState();
}
class _DepartmentFormDialogState extends State<_DepartmentFormDialog> {
late final TextEditingController _nameController;
late final TextEditingController _codeController;
late final TextEditingController _descriptionController;
bool _isActive = true;
@override
void initState() {
super.initState();
final department = widget.department;
_nameController = TextEditingController(text: department?.name ?? '');
_codeController = TextEditingController(text: department?.code ?? '');
_descriptionController = TextEditingController(text: department?.description ?? '');
_isActive = department?.isActive ?? true;
}
void _submit() {
if (_nameController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('部門名は必須です')));
return;
}
widget.onSubmit(
Department(
id: widget.department?.id ?? '',
name: _nameController.text.trim(),
code: _codeController.text.trim().isEmpty ? null : _codeController.text.trim(),
description: _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
isActive: _isActive,
updatedAt: DateTime.now(),
),
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.department == null ? '部門を追加' : '部門を編集'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: _nameController, decoration: const InputDecoration(labelText: '部門名 *')),
TextField(controller: _codeController, decoration: const InputDecoration(labelText: '部門コード')),
TextField(controller: _descriptionController, decoration: const InputDecoration(labelText: '説明'), maxLines: 2),
SwitchListTile(
title: const Text('稼働中'),
value: _isActive,
onChanged: (value) => setState(() => _isActive = value),
),
],
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('キャンセル')),
FilledButton(onPressed: _submit, child: const Text('保存')),
],
);
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.view_list, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(message),
],
),
);
}
}