h-1.flutter.4/@workspace/lib/widgets/master_edit_fields.dart

537 lines
No EOL
19 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.

// マスター編集用汎用ウィジェット(簡易実装)+ 電話帳連携対応
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:contacts_service/contacts_service.dart';
/// テキスト入力フィールド(マスター用)+ 電話帳取得ボタン付き
class MasterTextField extends StatefulWidget {
final String label;
final TextEditingController controller;
final TextInputType? keyboardType;
final bool readOnly;
final int? maxLines;
final String? hintText;
final String? initialValueText;
final String? phoneField; // 電話番号フィールド(電話帳取得用)
const MasterTextField({
super.key,
required this.label,
required this.controller,
this.keyboardType,
this.readOnly = false,
this.maxLines,
this.hintText,
this.initialValueText,
this.phoneField,
});
@override
State<MasterTextField> createState() => _MasterTextFieldState();
}
class _MasterTextFieldState extends State<MasterTextField> {
bool _isSearchingPhone = false;
String? _tempPhoneNumber;
Future<void> _searchContactsForPhone() async {
if (widget.readOnly) return;
// UI に待機表示を表示
setState(() {
_isSearchingPhone = true;
});
try {
final contacts = await ContactsService.getContacts(limit: 10);
final selectedContact = contacts.firstWhere(
(contact) => contact.phones.any((phone) =>
phone.number.replaceAll(' ', '').replaceAll('-', '').replaceAll('/', '') ==
widget.controller.text.replaceAll(' ', '').replaceAll('-', '').replaceAll('/', '')),
orElse: () => null,
);
if (selectedContact != null && selectedContact.phones.isNotEmpty) {
// 番号を一致するものを選択
final matchingPhone = selectedContact.phones.firstWhere(
(phone) => phone.number.replaceAll(' ', '').replaceAll('-', '').replaceAll('/', '') ==
widget.controller.text.replaceAll(' ', '').replaceAll('-', '').replaceAll('/', ''),
orElse: () => selectedContact.phones.first,
);
setState(() {
_tempPhoneNumber = matchingPhone.number;
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('連絡先検索に失敗しました:$e'), backgroundColor: Colors.orange),
);
}
} finally {
if (mounted) {
setState(() {
_isSearchingPhone = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text(widget.label, style: const TextStyle(fontWeight: FontWeight.bold))),
if (!widget.readOnly && widget.phoneField != null)
_buildPhoneSearchButton(),
],
),
SizedBox(height: 4),
Container(
constraints: BoxConstraints(maxHeight: 50), // セリ上がり病対策:最大高制限
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: widget.controller,
decoration: InputDecoration(
hintText: widget.hintText ?? (widget.initialValueText != null ? widget.initialValueText : null),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
keyboardType: widget.keyboardType,
readOnly: widget.readOnly,
maxLines: widget.maxLines ?? (widget.phoneField == null ? 1 : 2), // 電話番号時は最大 2 行
),
if (_isSearchingPhone && _tempPhoneNumber != null)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Icon(Icons.hourglass_empty, size: 16, color: Colors.orange),
SizedBox(width: 8),
Text('電話帳から取得中...', style: TextStyle(color: Colors.orange[700])),
],
),
),
if (_tempPhoneNumber != null)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Icon(Icons.check_circle, size: 16, color: Colors.green),
SizedBox(width: 8),
Text('見つかりました:$_tempPhoneNumber', style: TextStyle(color: Colors.green[700])),
IconButton(
icon: const Icon(Icons.close, size: 16),
onPressed: () {
setState(() {
_tempPhoneNumber = null;
});
},
),
],
),
),
],
),
),
],
),
);
}
Widget _buildPhoneSearchButton() {
return IconButton(
icon: const Icon(Icons.person_search),
tooltip: '電話帳から取得',
onPressed: _searchContactsForPhone,
);
}
}
/// 数値入力フィールド(マスター用)
class MasterNumberField extends StatelessWidget {
final String label;
final TextEditingController controller;
final String? hintText;
final bool readOnly;
final String? initialValueText;
const MasterNumberField({
super.key,
required this.label,
required this.controller,
this.hintText,
this.readOnly = false,
this.initialValueText,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Container(
constraints: BoxConstraints(maxHeight: 50), // セリ上がり病対策
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hintText ?? (initialValueText != null ? initialValueText : '1-5 の範囲3'),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
keyboardType: TextInputType.number,
readOnly: readOnly,
),
),
],
),
);
}
}
/// 日付入力フィールド(マスター用)
class MasterDateField extends StatelessWidget {
final String label;
final TextEditingController controller;
final DateTime? picked;
const MasterDateField({
super.key,
required this.label,
required this.controller,
this.picked,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Container(
constraints: BoxConstraints(maxHeight: 50), // セリ上がり病対策
child: TextField(
controller: controller,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
readOnly: true,
),
),
],
)),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
controller.text = DateFormat('yyyy/MM/dd').format(picked);
}
},
),
],
),
);
}
}
/// テキストフィールド用カスタムフォーカスードQR コード生成対応)
class MasterTextFieldNode extends FocusNode {
final GlobalKey key;
final String? value;
final bool isEditable;
MasterTextFieldNode({Key? key, this.value, this.isEditable = true}) : key = GlobalKey();
@override
FocusableState get state => key.currentState as FocusableState;
}
/// テキストフィールド用カスタムフォーカスードQR コード生成対応)
class MasterNumberFieldNode extends FocusNode {
final GlobalKey key;
final String? value;
final bool isEditable;
MasterNumberFieldNode({Key? key, this.value, this.isEditable = true}) : key = GlobalKey();
@override
FocusableState get state => key.currentState as FocusableState;
}
/// リッチマスターテキストフィールド(汎用性高く、他のマスタ参照可)
class RichMasterTextField extends StatelessWidget {
final String label;
final TextEditingController controller;
final TextInputType? keyboardType;
final bool readOnly;
final int? maxLines;
final String? hintText;
final String? initialValueText;
const RichMasterTextField({
super.key,
required this.label,
required this.controller,
this.keyboardType,
this.readOnly = false,
this.maxLines,
this.hintText,
this.initialValueText,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
if (initialValueText != null)
IconButton(
icon: const Icon(Icons.copy),
onPressed: () => _copyToClipboard(initialValueText!),
),
],
),
SizedBox(height: 4),
Container(
constraints: BoxConstraints(maxHeight: 50), // セリ上がり病対策
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hintText ?? (initialValueText != null ? initialValueText : null),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
keyboardType: keyboardType,
readOnly: readOnly,
maxLines: maxLines ?? 1,
),
),
],
),
);
}
void _copyToClipboard(String text) async {
try {
await Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('コピーしました'), backgroundColor: Colors.green),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('コピーに失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
/// リッチマスター日付フィールド(汎用性高く、他のマスタ参照可)
class RichMasterDateField extends StatelessWidget {
final String label;
final TextEditingController controller;
final DateTime? picked;
const RichMasterDateField({
super.key,
required this.label,
required this.controller,
this.picked,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
if (picked != null)
IconButton(
icon: const Icon(Icons.copy),
onPressed: () => _copyToDate(controller.text),
),
],
),
SizedBox(height: 4),
Container(
constraints: BoxConstraints(maxHeight: 50), // セリ上がり病対策
child: TextField(
controller: controller,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
readOnly: true,
),
),
],
)),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
controller.text = DateFormat('yyyy/MM/dd').format(picked);
}
},
),
],
),
);
}
void _copyToDate(String dateStr) async {
try {
await Clipboard.setData(ClipboardData(text: dateStr));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('日付をコピーしました'), backgroundColor: Colors.green),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('コピーに失敗:$e'), backgroundColor: Colors.red),
);
}
}
}
/// リッチマスター数値フィールド(汎用性高く、他のマスタ参照可)
class RichMasterNumberField extends StatelessWidget {
final String label;
final TextEditingController controller;
final String? hintText;
final bool readOnly;
final String? initialValueText;
const RichMasterNumberField({
super.key,
required this.label,
required this.controller,
this.hintText,
this.readOnly = false,
this.initialValueText,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
if (initialValueText != null)
IconButton(
icon: const Icon(Icons.copy),
onPressed: () => _copyToClipboard(initialValueText!),
),
],
),
SizedBox(height: 4),
Container(
constraints: BoxConstraints(maxHeight: 50), // セリ上がり病対策
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hintText ?? (initialValueText != null ? initialValueText : '1-5 の範囲3'),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
keyboardType: TextInputType.number,
readOnly: readOnly,
),
),
],
),
);
}
void _copyToClipboard(String text) async {
try {
await Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('コピーしました'), backgroundColor: Colors.green),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('コピーに失敗:$e'), backgroundColor: Colors.red),
);
}
}
}