309 lines
12 KiB
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),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|