h-1.flutter.4/lib/widgets/master_edit_dialog.dart

313 lines
No EOL
11 KiB
Dart
Raw Permalink 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: 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),
);
},
),
),
);
}
}