h-1.flutter.0/lib/screens/supplier_picker_modal.dart

309 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import '../models/supplier_model.dart';
import '../services/supplier_repository.dart';
import '../widgets/keyboard_inset_wrapper.dart';
import '../widgets/modal_utils.dart';
import '../widgets/screen_id_title.dart';
class SupplierPickerModal extends StatefulWidget {
const SupplierPickerModal({super.key, required this.onSupplierSelected});
final ValueChanged<Supplier> onSupplierSelected;
@override
State<SupplierPickerModal> createState() => _SupplierPickerModalState();
}
class _SupplierPickerModalState extends State<SupplierPickerModal> {
final SupplierRepository _repository = SupplierRepository();
final TextEditingController _searchController = TextEditingController();
final Uuid _uuid = const Uuid();
List<Supplier> _suppliers = const [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadSuppliers();
}
Future<void> _loadSuppliers([String keyword = '']) async {
setState(() => _isLoading = true);
final all = await _repository.fetchSuppliers(includeHidden: true);
final filtered = keyword.trim().isEmpty
? all
: all.where((s) => s.name.toLowerCase().contains(keyword.toLowerCase())).toList();
if (!mounted) return;
setState(() {
_suppliers = filtered;
_isLoading = false;
});
}
Future<void> _openEditor({Supplier? supplier}) async {
final result = await showFeatureModalBottomSheet<Supplier>(
context: context,
builder: (ctx) => _SupplierFormSheet(
supplier: supplier,
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.saveSupplier(saving);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('仕入先を保存しました')));
await _loadSuppliers(_searchController.text);
if (!mounted) return;
widget.onSupplierSelected(saving);
}
Future<void> _deleteSupplier(Supplier supplier) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('仕入先を削除'),
content: Text('${supplier.name} を削除しますか?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル')),
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('削除')),
],
),
);
if (confirmed != true) return;
await _repository.deleteSupplier(supplier.id);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('仕入先を削除しました')));
await _loadSuppliers(_searchController.text);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SafeArea(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
title: const ScreenAppBarTitle(screenId: 'P5', title: '仕入先選択'),
actions: [
IconButton(
tooltip: '仕入先を追加',
onPressed: _openEditor,
icon: const Icon(Icons.add_circle_outline),
),
],
),
body: KeyboardInsetWrapper(
safeAreaTop: false,
basePadding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
extraBottom: 24,
child: Column(
children: [
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '仕入先名で検索',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isEmpty
? null
: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_loadSuppliers('');
},
),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
isDense: true,
),
onChanged: _loadSuppliers,
),
const SizedBox(height: 12),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _suppliers.isEmpty
? Center(
child: Text(
'仕入先が見つかりません。右上の + から追加できます。',
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
)
: ListView.builder(
itemCount: _suppliers.length,
itemBuilder: (context, index) {
final supplier = _suppliers[index];
return Card(
child: ListTile(
title: Text(supplier.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (supplier.contactPerson?.isNotEmpty == true) Text('担当: ${supplier.contactPerson}'),
if (supplier.tel?.isNotEmpty == true) Text('TEL: ${supplier.tel}'),
],
),
onTap: () {
widget.onSupplierSelected(supplier);
Navigator.pop(context);
},
trailing: PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'edit':
_openEditor(supplier: supplier);
break;
case 'delete':
_deleteSupplier(supplier);
break;
}
},
itemBuilder: (context) => const [
PopupMenuItem(value: 'edit', child: Text('編集')),
PopupMenuItem(value: 'delete', child: Text('削除')),
],
),
),
);
},
),
),
],
),
),
),
),
);
}
}
class _SupplierFormSheet extends StatefulWidget {
const _SupplierFormSheet({required this.onSubmit, this.supplier});
final Supplier? supplier;
final ValueChanged<Supplier> onSubmit;
@override
State<_SupplierFormSheet> createState() => _SupplierFormSheetState();
}
class _SupplierFormSheetState extends State<_SupplierFormSheet> {
late final TextEditingController _nameController;
late final TextEditingController _contactController;
late final TextEditingController _telController;
late final TextEditingController _emailController;
late final TextEditingController _notesController;
final _formKey = GlobalKey<FormState>();
bool _isSaving = false;
@override
void initState() {
super.initState();
final supplier = widget.supplier;
_nameController = TextEditingController(text: supplier?.name ?? '');
_contactController = TextEditingController(text: supplier?.contactPerson ?? '');
_telController = TextEditingController(text: supplier?.tel ?? '');
_emailController = TextEditingController(text: supplier?.email ?? '');
_notesController = TextEditingController(text: supplier?.notes ?? '');
}
@override
void dispose() {
_nameController.dispose();
_contactController.dispose();
_telController.dispose();
_emailController.dispose();
_notesController.dispose();
super.dispose();
}
void _handleSubmit() {
if (_isSaving) return;
if (!_formKey.currentState!.validate()) return;
setState(() => _isSaving = true);
widget.onSubmit(
Supplier(
id: widget.supplier?.id ?? '',
name: _nameController.text.trim(),
contactPerson: _contactController.text.trim().isEmpty ? null : _contactController.text.trim(),
tel: _telController.text.trim().isEmpty ? null : _telController.text.trim(),
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
updatedAt: DateTime.now(),
),
);
}
@override
Widget build(BuildContext context) {
final title = widget.supplier == null ? '仕入先を追加' : '仕入先を編集';
return SafeArea(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: Material(
color: Colors.white,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
children: [
IconButton(onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close)),
const SizedBox(width: 4),
Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const Spacer(),
TextButton(
onPressed: _isSaving ? null : _handleSubmit,
child: _isSaving
? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('保存'),
),
],
),
),
Expanded(
child: KeyboardInsetWrapper(
safeAreaTop: false,
basePadding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
extraBottom: 24,
child: Form(
key: _formKey,
child: ListView(
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: '仕入先名 *'),
validator: (value) => value == null || value.trim().isEmpty ? '必須項目です' : null,
),
const SizedBox(height: 12),
TextFormField(controller: _contactController, decoration: const InputDecoration(labelText: '担当者')),
const SizedBox(height: 12),
TextFormField(controller: _telController, decoration: const InputDecoration(labelText: '電話番号')),
const SizedBox(height: 12),
TextFormField(controller: _emailController, decoration: const InputDecoration(labelText: 'メール')),
const SizedBox(height: 12),
TextFormField(controller: _notesController, decoration: const InputDecoration(labelText: '備考'), maxLines: 3),
],
),
),
),
),
],
),
),
),
);
}
}