// 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 createState() => _CalendarPickerDialogState(); } class _CalendarPickerDialogState extends State { 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); } }