268 lines
10 KiB
Dart
268 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import '../models/supplier_model.dart';
|
|
import '../services/supplier_repository.dart';
|
|
|
|
class SupplierMasterScreen extends StatefulWidget {
|
|
const SupplierMasterScreen({super.key});
|
|
|
|
@override
|
|
State<SupplierMasterScreen> createState() => _SupplierMasterScreenState();
|
|
}
|
|
|
|
class _SupplierMasterScreenState extends State<SupplierMasterScreen> {
|
|
final SupplierRepository _repository = SupplierRepository();
|
|
final Uuid _uuid = const Uuid();
|
|
|
|
bool _isLoading = true;
|
|
bool _showHidden = false;
|
|
List<Supplier> _suppliers = const [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSuppliers();
|
|
}
|
|
|
|
Future<void> _loadSuppliers() async {
|
|
setState(() => _isLoading = true);
|
|
final suppliers = await _repository.fetchSuppliers(includeHidden: _showHidden);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_suppliers = suppliers;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
|
|
Future<void> _openForm({Supplier? supplier}) async {
|
|
final result = await showDialog<Supplier>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) => _SupplierFormDialog(
|
|
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('仕入先を保存しました')));
|
|
_loadSuppliers();
|
|
}
|
|
|
|
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('削除', style: TextStyle(color: Colors.red))),
|
|
],
|
|
),
|
|
);
|
|
if (confirmed != true) return;
|
|
await _repository.deleteSupplier(supplier.id);
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('仕入先を削除しました')));
|
|
_loadSuppliers();
|
|
}
|
|
|
|
String _closingLabel(Supplier supplier) {
|
|
if (supplier.closingDay == null) return '締日未設定';
|
|
return '毎月${supplier.closingDay}日締め / ${supplier.paymentSiteDays}日サイト';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: const BackButton(),
|
|
title: const Text('M5:仕入先マスター'),
|
|
actions: [
|
|
Switch.adaptive(
|
|
value: _showHidden,
|
|
onChanged: (value) {
|
|
setState(() => _showHidden = value);
|
|
_loadSuppliers();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: () => _openForm(),
|
|
child: const Icon(Icons.add),
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _suppliers.isEmpty
|
|
? const _EmptyState(message: '仕入先が登録されていません')
|
|
: RefreshIndicator(
|
|
onRefresh: _loadSuppliers,
|
|
child: ListView.builder(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
itemCount: _suppliers.length,
|
|
itemBuilder: (context, index) {
|
|
final supplier = _suppliers[index];
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: ListTile(
|
|
title: Text(supplier.name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
subtitle: Text([
|
|
if (supplier.contactPerson?.isNotEmpty == true) '担当: ${supplier.contactPerson}',
|
|
if (supplier.tel?.isNotEmpty == true) 'TEL: ${supplier.tel}',
|
|
_closingLabel(supplier),
|
|
].where((e) => e.isNotEmpty).join('\n')),
|
|
trailing: PopupMenuButton<String>(
|
|
onSelected: (value) {
|
|
switch (value) {
|
|
case 'edit':
|
|
_openForm(supplier: supplier);
|
|
break;
|
|
case 'delete':
|
|
_deleteSupplier(supplier);
|
|
break;
|
|
}
|
|
},
|
|
itemBuilder: (context) => const [
|
|
PopupMenuItem(value: 'edit', child: Text('編集')),
|
|
PopupMenuItem(value: 'delete', child: Text('削除')),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SupplierFormDialog extends StatefulWidget {
|
|
const _SupplierFormDialog({required this.onSubmit, this.supplier});
|
|
|
|
final Supplier? supplier;
|
|
final ValueChanged<Supplier> onSubmit;
|
|
|
|
@override
|
|
State<_SupplierFormDialog> createState() => _SupplierFormDialogState();
|
|
}
|
|
|
|
class _SupplierFormDialogState extends State<_SupplierFormDialog> {
|
|
late final TextEditingController _nameController;
|
|
late final TextEditingController _contactController;
|
|
late final TextEditingController _emailController;
|
|
late final TextEditingController _telController;
|
|
late final TextEditingController _addressController;
|
|
late final TextEditingController _paymentSiteController;
|
|
late final TextEditingController _notesController;
|
|
int? _closingDay;
|
|
bool _isHidden = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final supplier = widget.supplier;
|
|
_nameController = TextEditingController(text: supplier?.name ?? '');
|
|
_contactController = TextEditingController(text: supplier?.contactPerson ?? '');
|
|
_emailController = TextEditingController(text: supplier?.email ?? '');
|
|
_telController = TextEditingController(text: supplier?.tel ?? '');
|
|
_addressController = TextEditingController(text: supplier?.address ?? '');
|
|
_paymentSiteController = TextEditingController(text: (supplier?.paymentSiteDays ?? 30).toString());
|
|
_notesController = TextEditingController(text: supplier?.notes ?? '');
|
|
_closingDay = supplier?.closingDay;
|
|
_isHidden = supplier?.isHidden ?? false;
|
|
}
|
|
|
|
void _submit() {
|
|
if (_nameController.text.trim().isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('仕入先名は必須です')));
|
|
return;
|
|
}
|
|
final paymentSite = int.tryParse(_paymentSiteController.text) ?? 30;
|
|
widget.onSubmit(
|
|
Supplier(
|
|
id: widget.supplier?.id ?? '',
|
|
name: _nameController.text.trim(),
|
|
contactPerson: _contactController.text.trim().isEmpty ? null : _contactController.text.trim(),
|
|
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
|
tel: _telController.text.trim().isEmpty ? null : _telController.text.trim(),
|
|
address: _addressController.text.trim().isEmpty ? null : _addressController.text.trim(),
|
|
closingDay: _closingDay,
|
|
paymentSiteDays: paymentSite,
|
|
notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
|
|
isHidden: _isHidden,
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final closingOptions = [null, ...List<int>.generate(31, (index) => index + 1)];
|
|
return AlertDialog(
|
|
title: Text(widget.supplier == null ? '仕入先を追加' : '仕入先を編集'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(controller: _nameController, decoration: const InputDecoration(labelText: '仕入先名 *')),
|
|
TextField(controller: _contactController, decoration: const InputDecoration(labelText: '担当者')),
|
|
TextField(controller: _emailController, decoration: const InputDecoration(labelText: 'メール'), keyboardType: TextInputType.emailAddress),
|
|
TextField(controller: _telController, decoration: const InputDecoration(labelText: '電話番号'), keyboardType: TextInputType.phone),
|
|
TextField(controller: _addressController, decoration: const InputDecoration(labelText: '住所')),
|
|
DropdownButtonFormField<int?>(
|
|
initialValue: _closingDay,
|
|
items: closingOptions
|
|
.map((day) => DropdownMenuItem(
|
|
value: day,
|
|
child: Text(day == null ? '締日未設定' : '$day日締め'),
|
|
))
|
|
.toList(),
|
|
onChanged: (val) => setState(() => _closingDay = val),
|
|
decoration: const InputDecoration(labelText: '締日'),
|
|
),
|
|
TextField(
|
|
controller: _paymentSiteController,
|
|
decoration: const InputDecoration(labelText: '支払サイト(日)'),
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
TextField(controller: _notesController, decoration: const InputDecoration(labelText: '備考'), maxLines: 2),
|
|
SwitchListTile(
|
|
title: const Text('非表示にする'),
|
|
value: _isHidden,
|
|
onChanged: (val) => setState(() => _isHidden = val),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
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.inbox, size: 64, color: Colors.grey),
|
|
const SizedBox(height: 16),
|
|
Text(message),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|