313 lines
No EOL
11 KiB
Dart
313 lines
No EOL
11 KiB
Dart
// Version: 3.5 - リッチマスター編集ダイアログ(改良版)
|
||
// ※ 汎用性の高いリッチなマスター編集ダイアログ(全てのマスタで共通使用)
|
||
import 'package:flutter/material.dart';
|
||
import '../models/product.dart';
|
||
|
||
/// 汎用性の高いリッチなマスター編集ダイアログ
|
||
class MasterEditDialog<T extends Product> extends StatefulWidget {
|
||
final String title;
|
||
final T? initialData;
|
||
final bool showStatusFields;
|
||
final Function(T)? onSave;
|
||
|
||
const MasterEditDialog({
|
||
super.key,
|
||
required this.title,
|
||
this.initialData,
|
||
this.showStatusFields = false,
|
||
this.onSave,
|
||
});
|
||
|
||
@override
|
||
State<MasterEditDialog> createState() => _MasterEditDialogState();
|
||
}
|
||
|
||
class _MasterEditDialogState extends State<MasterEditDialog> {
|
||
late TextEditingController codeController;
|
||
late TextEditingController nameController;
|
||
late TextEditingController addressController;
|
||
late TextEditingController phoneController;
|
||
late TextEditingController emailController;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
final data = widget.initialData;
|
||
// デフォルトのサンプル値
|
||
codeController = TextEditingController(text: data?.productCode ?? '');
|
||
nameController = TextEditingController(text: data?.name ?? '');
|
||
addressController = TextEditingController(text: data?.address ?? '');
|
||
phoneController = TextEditingController(text: data?.phone ?? '');
|
||
emailController = TextEditingController(text: data?.email ?? '');
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
codeController.dispose();
|
||
nameController.dispose();
|
||
addressController.dispose();
|
||
phoneController.dispose();
|
||
emailController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
/// リッチな入力フィールドビルダー
|
||
Widget _buildRichTextField(
|
||
String label,
|
||
TextEditingController controller, {
|
||
TextInputType? keyboard,
|
||
IconData? icon,
|
||
String hint = '',
|
||
}) {
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: Colors.grey.shade700),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: Theme.of(context).dividerColor),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: TextField(
|
||
controller: controller,
|
||
keyboardType: keyboard,
|
||
style: const TextStyle(fontSize: 14),
|
||
decoration: InputDecoration(
|
||
hintText: hint.isEmpty ? null : hint,
|
||
prefixIcon: Icon(icon, size: 16, color: Theme.of(context).primaryColor),
|
||
border: InputBorder.none,
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Dialog(
|
||
backgroundColor: Colors.white,
|
||
child: Container(
|
||
constraints: const BoxConstraints(maxWidth: 420),
|
||
padding: const EdgeInsets.all(16),
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
// タイトル
|
||
Row(
|
||
children: [
|
||
Icon(Icons.edit, size: 20, color: Theme.of(context).primaryColor),
|
||
const SizedBox(width: 8),
|
||
Expanded(child: Text(
|
||
widget.title,
|
||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||
)),
|
||
IconButton(
|
||
icon: Icon(Icons.close, color: Colors.grey),
|
||
onPressed: () => Navigator.pop(context),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
|
||
// ヒントテキスト
|
||
Center(
|
||
child: Text(
|
||
'新規作成の場合は「空白」から入力して OK を押してください',
|
||
style: TextStyle(fontSize: 12, color: Colors.grey.shade500, fontStyle: FontStyle.italic),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// リッチな編集フォーム
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).cardColor,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Theme.of(context).dividerColor),
|
||
),
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 基本情報セクション
|
||
Text(
|
||
'■ 基本情報',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: Theme.of(context).primaryColor,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// コードフィールド
|
||
_buildRichTextField(
|
||
'コード *',
|
||
codeController,
|
||
keyboard: TextInputType.text,
|
||
icon: Icons.code,
|
||
hint: e.g., 'P001',
|
||
),
|
||
|
||
// 名称フィールド
|
||
_buildRichTextField(
|
||
'商品名 / 会社名 *',
|
||
nameController,
|
||
keyboard: TextInputType.name,
|
||
icon: Icons.business,
|
||
hint: e.g., 'サンプル製品 A',
|
||
),
|
||
|
||
// アドレスフィールド(オプション)
|
||
_buildRichTextField(
|
||
'住所',
|
||
addressController,
|
||
keyboard: TextInputType.text,
|
||
icon: Icons.location_on,
|
||
hint: '省略可',
|
||
),
|
||
|
||
if (widget.showStatusFields) ...[
|
||
const SizedBox(height: 8),
|
||
const Divider(),
|
||
const SizedBox(height: 4),
|
||
// ステータス情報セクション
|
||
Text(
|
||
'■ ステータス情報',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.bold,
|
||
color: Theme.of(context).primaryColor,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
_buildRichTextField(
|
||
'電話番号',
|
||
phoneController,
|
||
keyboard: TextInputType.phone,
|
||
icon: Icons.phone,
|
||
hint: '03-1234-5678',
|
||
),
|
||
|
||
_buildRichTextField(
|
||
'E メール',
|
||
emailController,
|
||
keyboard: TextInputType.emailAddress,
|
||
icon: Icons.email,
|
||
hint: 'example@example.com',
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
|
||
const SizedBox(height: 16),
|
||
|
||
// アクションボタン(Flex で配置)
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: OutlinedButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
style: OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
),
|
||
child: Text(' キャンセル ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
flex: 3, // より広いボタン
|
||
child: ElevatedButton(
|
||
onPressed: () {
|
||
// TODO: onSave コールバックを実装
|
||
if (widget.onSave != null) {
|
||
widget.onSave!(widget.initialData as T);
|
||
} else {
|
||
Navigator.pop(context);
|
||
}
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
),
|
||
child: Text(' 保存 ', textAlign: TextAlign.center, style: TextStyle(fontSize: 15)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 参照マスタ選択ダイアログ(リッチ版)
|
||
class SingleChoiceDialog extends StatelessWidget {
|
||
final List<Product> items;
|
||
final Function() onCancel;
|
||
final Function(Product) onSelected;
|
||
|
||
const SingleChoiceDialog({super.key, required this.items, required this.onCancel, required this.onSelected});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (items.isEmpty) return Dialog(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(32),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.search_off, size: 64, color: Colors.grey[300]),
|
||
const SizedBox(height: 16),
|
||
Text('検索結果がありません', style: TextStyle(color: Colors.grey, fontSize: 18)),
|
||
const SizedBox(height: 8),
|
||
Text('マスタデータが登録されていないため\n参照できません', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade500)),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
return Dialog(
|
||
child: Container(
|
||
constraints: const BoxConstraints(maxWidth: 400),
|
||
padding: const EdgeInsets.all(12),
|
||
child: ListView.separated(
|
||
shrinkWrap: true,
|
||
itemCount: items.length,
|
||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||
itemBuilder: (ctx, index) {
|
||
final item = items[index];
|
||
return ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: CircleAvatar(
|
||
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
|
||
child: Icon(Icons.inventory_2, color: Theme.of(context).primaryColor),
|
||
),
|
||
title: Text(
|
||
item.name ?? '未入力',
|
||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||
),
|
||
subtitle: Text(item.productCode ?? '', style: TextStyle(fontSize: 12)),
|
||
onTap: () => onSelected(item),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |