h-1.flutter.4/@workspace/lib/screens/master/supplier_master_screen.dart

474 lines
No EOL
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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),
],
),
),
],
),
);
}
}