474 lines
No EOL
18 KiB
Dart
474 lines
No EOL
18 KiB
Dart
// Version: 2.8 - 簡易仕入先マスタ(電話帳対応)
|
||
import 'package:flutter/material.dart';
|
||
import 'package:intl/intl.dart';
|
||
import '../../models/product.dart';
|
||
import '../../services/database_helper.dart';
|
||
import '../../widgets/master_edit_fields.dart';
|
||
|
||
/// 簡易仕入先マスタ管理画面 + 電話帳連携対応
|
||
class RichSupplierMasterScreen extends StatefulWidget {
|
||
const RichSupplierMasterScreen({super.key});
|
||
|
||
@override
|
||
State<RichSupplierMasterScreen> createState() => _RichSupplierMasterScreenState();
|
||
}
|
||
|
||
class _RichSupplierMasterScreenState extends State<RichSupplierMasterScreen> {
|
||
final DatabaseHelper _db = DatabaseHelper.instance;
|
||
List<Product> _suppliers = [];
|
||
bool _isLoading = true;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadSuppliers();
|
||
}
|
||
|
||
Future<void> _loadSuppliers() async {
|
||
try {
|
||
final products = await _db.getProducts();
|
||
if (mounted) setState(() {
|
||
_suppliers = products ?? const <Product>[];
|
||
_isLoading = false;
|
||
});
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('仕入先データを読み込みませんでした:$e'), backgroundColor: Colors.red),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _addSupplier(Product supplier) async {
|
||
try {
|
||
await _db.insertProduct(supplier);
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('仕入先を登録しました'), backgroundColor: Colors.green),
|
||
);
|
||
await _loadSuppliers();
|
||
}
|
||
} catch (e) {
|
||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('登録に失敗:$e'), backgroundColor: Colors.red),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _editSupplier(Product supplier) async {
|
||
if (!mounted) return;
|
||
try {
|
||
final updatedSupplier = await _showEditDialog(context, supplier);
|
||
if (updatedSupplier != null && mounted) {
|
||
await _db.updateProduct(updatedSupplier);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('仕入先を更新しました'), backgroundColor: Colors.green),
|
||
);
|
||
await _loadSuppliers();
|
||
}
|
||
} catch (e) {
|
||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('更新に失敗:$e'), backgroundColor: Colors.red),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _deleteSupplier(int id) async {
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('仕入先削除'),
|
||
content: Text('この仕入先を削除しますか?'),
|
||
actions: [
|
||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('キャンセル')),
|
||
ElevatedButton(
|
||
onPressed: () => Navigator.pop(ctx, true),
|
||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||
child: const Text('削除'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (confirmed == true) {
|
||
try {
|
||
// TODO: データベースから削除ロジックを実装(現在は簡易実装)
|
||
if (mounted) _loadSuppliers();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('仕入先を削除しました'), backgroundColor: Colors.green),
|
||
);
|
||
} catch (e) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('削除に失敗:$e'), backgroundColor: Colors.red),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<Product?> _showEditDialog(BuildContext context, Product supplier) async {
|
||
return showDialog<Product>(
|
||
context: context,
|
||
builder: (ctx) => Dialog(
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Text(
|
||
'仕入先情報',
|
||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
|
||
MasterTextField(
|
||
label: '製品コード *',
|
||
controller: TextEditingController(text: supplier.productCode ?? ''),
|
||
hintText: '例:S-001, SAN-002 など(半角英数字)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: '会社名 *',
|
||
controller: TextEditingController(text: supplier.name ?? ''),
|
||
hintText: '例:株式会社〇〇商事、個人商社で可',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: '担当者名',
|
||
controller: TextEditingController(text: supplier.supplierContactName.isNotEmpty ? supplier.supplierContactName : ''),
|
||
hintText: '例:田中太郎(日本語漢字可)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 電話番号フィールド - 電話帳連携対応
|
||
MasterTextField(
|
||
label: '電話番号',
|
||
controller: TextEditingController(text: supplier.supplierPhoneNumber.isNotEmpty ? supplier.supplierPhoneNumber : ''),
|
||
hintText: '例:03-1234-5678、区切り不要(0312345678)',
|
||
keyboardType: TextInputType.phone,
|
||
phoneField: 'supplierPhoneNumber', // 電話帳連携用
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: 'メールアドレス',
|
||
controller: TextEditingController(text: supplier.email.isNotEmpty ? supplier.email : ''),
|
||
hintText: '@example.com の形式(例:order@ooshouki.co.jp)',
|
||
keyboardType: TextInputType.emailAddress,
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: '住所',
|
||
controller: TextEditingController(text: supplier.address.isNotEmpty ? supplier.address : ''),
|
||
hintText: '〒000-0000 市区町村名・番地・建物名',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 評価ポイント(1-5)
|
||
MasterNumberField(
|
||
label: '評価ポイント *',
|
||
controller: TextEditingController(text: supplier.quantity.toString()),
|
||
hintText: '1-5 の範囲(例:5 は最高レベル)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterDateField(
|
||
label: '登録日',
|
||
controller: TextEditingController(text: DateFormat('yyyy/MM/dd').format(supplier.createdAt ?? DateTime.now())),
|
||
),
|
||
|
||
const SizedBox(height: 24),
|
||
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Text(
|
||
'保存',
|
||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
|
||
SizedBox(height: 24, width: double.infinity, child: ElevatedButton(
|
||
onPressed: () => Navigator.pop(ctx, supplier),
|
||
style: ElevatedButton.styleFrom(backgroundColor: Theme.of(context).primaryColor, padding: const EdgeInsets.symmetric(vertical: 16)),
|
||
child: const Text('保存', style: TextStyle(fontSize: 16)),
|
||
)),
|
||
|
||
SizedBox(height: 8, width: double.infinity, child: OutlinedButton(
|
||
onPressed: () => Navigator.pop(ctx),
|
||
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
|
||
child: const Text('キャンセル'),
|
||
)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _showSupplierDetail(BuildContext context, Product supplier) async {
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('仕入先詳細'),
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (supplier.productCode.isNotEmpty) _detailRow('製品コード', supplier.productCode),
|
||
if (supplier.name.isNotEmpty) _detailRow('会社名', supplier.name),
|
||
if (supplier.supplierContactName.isNotEmpty) _detailRow('担当者名', supplier.supplierContactName),
|
||
_detailRow('電話番号', supplier.supplierPhoneNumber.isNotEmpty ? supplier.supplierPhoneNumber : '-'),
|
||
if (supplier.email.isNotEmpty) _detailRow('Email', supplier.email),
|
||
if (supplier.address.isNotEmpty) _detailRow('住所', supplier.address),
|
||
_detailRow('評価ポイント', '★'.repeat(supplier.quantity.toInt() > 0 ? supplier.quantity.toInt() : 1)),
|
||
_detailRow('登録日', DateFormat('yyyy/MM/dd').format(supplier.createdAt)),
|
||
],
|
||
),
|
||
),
|
||
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('閉じる'))],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _detailRow(String label, String value) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(width: 100),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
|
||
if (value != '-') Text(value),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text('/M3. 仕入先マスタ'),
|
||
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _loadSuppliers)],
|
||
),
|
||
body: _isLoading ? const Center(child: CircularProgressIndicator()) :
|
||
_suppliers.isEmpty ? Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]),
|
||
SizedBox(height: 16),
|
||
Text('仕入先データがありません', style: TextStyle(color: Colors.grey)),
|
||
SizedBox(height: 16),
|
||
FloatingActionButton.extended(
|
||
icon: Icon(Icons.add, color: Theme.of(context).primaryColor),
|
||
label: const Text('新規登録'),
|
||
onPressed: () => _showAddDialog(context),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: ListView.builder(
|
||
padding: const EdgeInsets.all(8),
|
||
itemCount: _suppliers.length,
|
||
itemBuilder: (context, index) {
|
||
final supplier = _suppliers[index];
|
||
return Card(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
elevation: 4,
|
||
child: ListTile(
|
||
leading: CircleAvatar(backgroundColor: Colors.orange.shade100, child: Icon(Icons.business, color: Colors.orange)),
|
||
title: Text(supplier.name ?? '未入力'),
|
||
subtitle: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (supplier.productCode.isNotEmpty) Text('製品コード:${supplier.productCode}', style: const TextStyle(fontSize: 12)),
|
||
if (supplier.supplierPhoneNumber.isNotEmpty) Text('電話:${supplier.supplierPhoneNumber}', style: const TextStyle(fontSize: 12)),
|
||
if (supplier.email.isNotEmpty) Text('Email: ${supplier.email}', style: const TextStyle(fontSize: 12)),
|
||
Text('評価:★'.repeat(supplier.quantity.toInt() > 0 ? supplier.quantity.toInt() : 1), style: const TextStyle(color: Colors.orange, fontSize: 12)),
|
||
],
|
||
),
|
||
trailing: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
IconButton(icon: Icon(Icons.edit, color: Colors.blue), onPressed: () => _editSupplier(supplier)),
|
||
PopupMenuButton<String>(
|
||
onSelected: (value) => value == 'delete' ? _deleteSupplier(supplier.id ?? 0) : null,
|
||
itemBuilder: (ctx) => [
|
||
const PopupMenuItem(child: Text('詳細'), onPressed: () => _showSupplierDetail(context, supplier)),
|
||
const PopupMenuItem(child: Text('削除'), onPressed: () => _deleteSupplier(supplier.id ?? 0)),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
floatingActionButton: FloatingActionButton.extended(
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('新規登録'),
|
||
onPressed: () => _showAddDialog(context),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showAddDialog(BuildContext context) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => Dialog(
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Text(
|
||
'新規仕入先登録',
|
||
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
|
||
MasterTextField(
|
||
label: '製品コード *',
|
||
controller: TextEditingController(),
|
||
hintText: '例:S-001, SAN-002 など(半角英数字)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: '会社名 *',
|
||
controller: TextEditingController(),
|
||
hintText: '例:株式会社〇〇商事、個人商社で可',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: '担当者名',
|
||
controller: TextEditingController(),
|
||
hintText: '例:田中太郎(日本語漢字可)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// 電話番号フィールド - 電話帳連携対応
|
||
MasterTextField(
|
||
label: '電話番号',
|
||
controller: TextEditingController(),
|
||
keyboardType: TextInputType.phone,
|
||
hintText: '例:03-1234-5678、区切り不要(0312345678)',
|
||
phoneField: 'supplierPhoneNumber', // 電話帳連携用
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: 'メールアドレス',
|
||
controller: TextEditingController(),
|
||
keyboardType: TextInputType.emailAddress,
|
||
hintText: '@example.com の形式(例:order@ooshouki.co.jp)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterTextField(
|
||
label: '住所',
|
||
controller: TextEditingController(),
|
||
hintText: '〒000-0000 市区町村名・番地・建物名',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterNumberField(
|
||
label: '評価ポイント *',
|
||
controller: TextEditingController(),
|
||
hintText: '1-5 の範囲(例:3)',
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
MasterDateField(
|
||
label: '登録日',
|
||
controller: TextEditingController(text: DateFormat('yyyy/MM/dd').format(DateTime.now())),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 仕入先マスタ用詳細表示ダイアログ(電話帳対応)
|
||
class SupplierDetailDialog extends StatelessWidget {
|
||
final Product supplier;
|
||
|
||
const SupplierDetailDialog({super.key, required this.supplier});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AlertDialog(
|
||
title: const Text('仕入先詳細'),
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_detailRow('製品コード', supplier.productCode ?? '-'),
|
||
_detailRow('会社名', supplier.name ?? '-'),
|
||
if (supplier.supplierContactName.isNotEmpty) _detailRow('担当者名', supplier.supplierContactName),
|
||
_detailRow('電話番号', supplier.supplierPhoneNumber.isNotEmpty ? supplier.supplierPhoneNumber : '-'),
|
||
if (supplier.email.isNotEmpty) _detailRow('Email', supplier.email),
|
||
if (supplier.address.isNotEmpty) _detailRow('住所', supplier.address),
|
||
_detailRow('評価ポイント', '★'.repeat(supplier.quantity.toInt() > 0 ? supplier.quantity.toInt() : 1)),
|
||
_detailRow('登録日', DateFormat('yyyy/MM/dd').format(supplier.createdAt)),
|
||
],
|
||
),
|
||
),
|
||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('閉じる'))],
|
||
);
|
||
}
|
||
|
||
Widget _detailRow(String label, String value) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(width: 100),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label, style: TextStyle(fontWeight: FontWeight.bold)),
|
||
if (value != '-') Text(value),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
} |