h-1.flutter.4/@

416 lines
No EOL
14 KiB
Text
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: 2.0 - マスター編集用汎用ウィジェット(簡易実装・互換性保持)
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'master_edit_fields.dart';
/// 簡易テキストフィールドMasterTextField 互換)
class MasterTextField extends StatelessWidget {
final String label;
final String? hint;
final TextEditingController controller;
final String? Function(String?)? validator;
final bool readOnly;
const MasterTextField({
super.key,
required this.label,
this.hint,
required this.controller,
this.validator,
this.readOnly = false,
});
@override
Widget build(BuildContext context) {
return RichMasterTextField(
label: label,
initialValue: controller.text.isEmpty ? null : controller.text,
hintText: hint ?? '値を入力してください',
readOnly: readOnly,
);
}
}
/// 簡易数値フィールドMasterNumberField 互換)
class MasterNumberField extends StatelessWidget {
final String label;
final String? hint;
final TextEditingController controller;
final String? Function(String?)? validator;
final bool readOnly;
const MasterNumberField({
super.key,
required this.label,
this.hint,
required this.controller,
this.validator,
this.readOnly = false,
});
@override
Widget build(BuildContext context) {
return RichMasterNumberField(
label: label,
initialValue: double.tryParse(controller.text ?? '') != null ? controller.text : null,
hintText: hint ?? '0.00',
readOnly: readOnly,
);
}
}
/// テキストフィールド(リッチ機能)
class RichMasterTextField extends StatelessWidget {
final String label;
final String? initialValue;
final String? hintText;
final bool readOnly;
const RichMasterTextField({
super.key,
required this.label,
this.initialValue,
this.hintText,
this.readOnly = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
),
if (initialValue != null) ...[
Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'入力済',
style: TextStyle(fontSize: 10, color: Colors.green.shade700),
),
),
],
],
),
TextField(
controller: TextEditingController(text: initialValue ?? ''),
decoration: InputDecoration(
hintText: hintText,
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey[400]),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: readOnly ? BorderSide.none : BorderSide(),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(color: Theme.of(context).dividerColor),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(color: Theme.of(context).primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
enabled: !readOnly,
),
],
),
);
}
}
/// 数値フィールド(リッチ機能:自動補完・フォーマット)
class RichMasterNumberField extends StatelessWidget {
final String label;
final double? initialValue;
final String? hintText;
final bool readOnly;
const RichMasterNumberField({
super.key,
required this.label,
this.initialValue,
this.hintText,
this.readOnly = false,
});
@override
Widget build(BuildContext context) {
final formatter = NumberFormat('#,##0.00', 'ja_JP');
String formattedValue = initialValue?.toString() ?? '';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
),
if (initialValue != null && initialValue!.isFinite) ...[
Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
formatter.format(initialValue!),
style: TextStyle(fontSize: 10, color: Colors.blue.shade700),
),
),
],
],
),
TextField(
controller: TextEditingController(text: formattedValue.isEmpty ? null : formattedValue),
decoration: InputDecoration(
hintText: hintText,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
),
keyboardType: TextInputType.number,
),
],
),
);
}
}
/// 日付フィールド(リッチ機能:年次表示・カレンダーピッカー)
class RichMasterDateField extends StatelessWidget {
final String label;
final DateTime? initialValue;
final String? hintText;
final bool readOnly;
const RichMasterDateField({
super.key,
required this.label,
this.initialValue,
this.hintText,
this.readOnly = false,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
),
if (initialValue != null) ...[
Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.purple.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
DateFormat('yyyy/MM/dd').format(initialValue!),
style: TextStyle(fontSize: 10, color: Colors.purple.shade700),
),
),
],
],
),
TextField(
readOnly: readOnly,
decoration: InputDecoration(
hintText: hintText,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 16.0),
suffixIcon: initialValue != null && !readOnly ? IconButton(
icon: Icon(Icons.calendar_today),
onPressed: () => _showDatePicker(context),
) : null,
),
),
],
),
);
}
void _showDatePicker(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (ctx) => CalendarPickerDialog(initialDate: initialValue ?? DateTime.now()),
);
}
}
/// カレンダーピッカー(年次表示機能)
class CalendarPickerDialog extends StatefulWidget {
final DateTime initialDate;
const CalendarPickerDialog({super.key, required this.initialDate});
@override
State<CalendarPickerDialog> createState() => _CalendarPickerDialogState();
}
class _CalendarPickerDialogState extends State<CalendarPickerDialog> {
int _year = widget.initialDate.year;
int _month = widget.initialDate.month - 1;
DateTime get _selectedDate => DateTime(_year, _month + 1);
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
initialChildSize: 0.5,
minChildSize: 0.3,
maxChildSize: 0.9,
builder: (context, scrollController) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 4)],
),
child: Row(
children: [
IconButton(icon: Icon(Icons.chevron_left), onPressed: _changeMonth),
Text(_formatYear(_year), style: Theme.of(context).textTheme.titleLarge),
IconButton(icon: Icon(Icons.chevron_right), onPressed: _changeMonth),
Spacer(),
ElevatedButton.icon(
onPressed: () => setState(() {
_year = DateTime.now().year;
_month = DateTime.now().month - 1;
}),
icon: Icon(Icons.check),
label: Text('現在'),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8),
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
childAspectRatio: 1.5,
),
itemCount: 6 * 35 + 2,
itemBuilder: (context, index) {
final monthIndex = index ~/ 35;
final dayOfWeek = index % 7;
if (monthIndex >= 6) return const SizedBox();
final monthDay = DateTime(_year, monthIndex + 1);
final dayOfMonthIndex = (monthDay.weekday - 1 + _selectedDate.weekday - 1) % 7;
final isCurrentMonth = dayOfWeek == dayOfMonthIndex && monthIndex == 0;
return Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isCurrentMonth ? Colors.blue.shade50 : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
dayOfMonthIndex + 1 < 10 ? '0$dayOfMonthIndex' : '$dayOfMonthIndex',
style: TextStyle(
fontWeight: isCurrentMonth ? FontWeight.bold : FontStyle.normal,
),
),
);
},
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 48,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 6 * 35 + 2,
itemBuilder: (context, index) {
final monthIndex = index ~/ 35;
final dayOfWeek = index % 7;
if (monthIndex >= 6) return const SizedBox();
final monthDay = DateTime(_year, monthIndex + 1);
final dayOfMonthIndex = (monthDay.weekday - 1 + _selectedDate.weekday - 1) % 7;
final isCurrentMonth = dayOfWeek == dayOfMonthIndex && monthIndex == 0;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ElevatedButton.icon(
onPressed: () {
if (mounted) setState(() {
_year = _selectedDate.year;
_month = _selectedDate.month - 1;
});
Navigator.pop(context);
},
icon: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
'${isCurrentMonth ? "" : "月"}${dayOfMonthIndex + 1}',
style: TextStyle(fontWeight: isCurrentMonth ? FontWeight.bold : FontWeight.normal),
),
),
label: const SizedBox(width: 32),
),
);
},
),
),
),
],
);
},
);
}
String _formatYear(int year) {
return year > DateTime.now().year ? '${year - DateTime.now().year}年先' : year;
}
void _changeMonth() {
setState(() => _month = (_month + 1) % 12);
}
}