import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../../app/di/injection.dart'; import '../../../../features/calendar/data/repositories/calendar_repository.dart'; import '../../../../features/calendar/data/models/schedule_item_model.dart'; import '../../../../core/l10n/l10n.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_pressable.dart'; import '../../../../shared/widgets/app_sheet_input_field.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/error_retry_surface.dart'; import '../../../../shared/widgets/full_screen_loading.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/apis/todo_api.dart'; class TodoEditScreen extends StatefulWidget { final String? todoId; const TodoEditScreen({super.key, required this.todoId}); const TodoEditScreen.create({super.key}) : todoId = null; bool get isCreateMode => todoId == null; @override State createState() => _TodoEditScreenState(); } class _TodoEditScreenState extends State { final TodoApi _todoApi = sl(); final CalendarRepository _calendarRepository = sl(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); TodoResponse? _todo; bool _loading = true; bool _saving = false; String? _error; int _priority = 1; final Set _selectedScheduleItemIds = {}; List<_ScheduleItemSimple> _scheduleItems = const <_ScheduleItemSimple>[]; ColorScheme get _colorScheme => Theme.of(context).colorScheme; @override void initState() { super.initState(); _loadPage(); } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); super.dispose(); } Future _loadPage() async { setState(() { _loading = true; _error = null; }); try { final now = DateTime.now(); final start = now.subtract(const Duration(days: 30)); final end = now.add(const Duration(days: 90)); final scheduleItems = await _calendarRepository.listByRange( startAt: start, endAt: end, ); TodoResponse? todo; if (!widget.isCreateMode) { todo = await _todoApi.getTodo(widget.todoId!); } if (!mounted) { return; } _todo = todo; _titleController.text = todo?.title ?? ''; _descriptionController.text = todo?.description ?? ''; _priority = todo?.priority ?? 1; _selectedScheduleItemIds ..clear() ..addAll(todo?.scheduleItems.map((item) => item.id) ?? const []); _scheduleItems = scheduleItems .where((item) => item.status == ScheduleStatus.active) .map( (item) => _ScheduleItemSimple( id: item.id, title: item.title, startAt: item.startAt, ), ) .toList(); setState(() { _loading = false; }); } catch (error) { if (!mounted) { return; } setState(() { _loading = false; _error = error.toString(); }); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _colorScheme.surface, resizeToAvoidBottomInset: false, body: SafeArea( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusScope.of(context).unfocus(), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ _colorScheme.surfaceContainerLow, _colorScheme.surface, ], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ BackTitlePageHeader( title: widget.isCreateMode ? context.l10n.todoCreateTitle : context.l10n.todoEditTitle, ), Expanded(child: _buildBody()), _buildBottomAction(), ], ), ), ), ), ); } Widget _buildBody() { if (_loading) { return const FullScreenLoading(); } if (_error != null) { return ErrorRetrySurface( message: context.l10n.commonLoadFailed(_error!), onRetry: _loadPage, ); } if (!widget.isCreateMode && _todo == null) { return Center(child: Text(context.l10n.todoNotFound)); } return ListView( padding: const EdgeInsets.fromLTRB( AppSpacing.lg, AppSpacing.sm, AppSpacing.lg, AppSpacing.lg, ), children: [ _buildHeaderCard(), const SizedBox(height: AppSpacing.md), _buildFormCard(), const SizedBox(height: AppSpacing.md), _buildScheduleCard(), ], ); } Widget _buildHeaderCard() { final headerDesc = widget.isCreateMode ? context.l10n.todoInfoDescCreate : _todo?.status == 'done' ? context.l10n.todoInfoDescDone : context.l10n.todoInfoDescDefault; return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: _colorScheme.outlineVariant), boxShadow: [ BoxShadow( color: _colorScheme.shadow.withValues(alpha: 0.18), blurRadius: AppRadius.lg, offset: const Offset(0, AppSpacing.xs), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.todoInfoTitle, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( headerDesc, style: TextStyle( fontSize: 13, color: _colorScheme.onSurfaceVariant, ), ), ], ), ); } Widget _buildFormCard() { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: _colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AppSheetInputField( controller: _titleController, label: context.l10n.todoFieldTitle, hint: context.l10n.todoFieldTitleHint, ), const SizedBox(height: AppSpacing.lg), AppSheetInputField( controller: _descriptionController, label: context.l10n.todoFieldDescriptionOptional, hint: context.l10n.todoFieldDescriptionHint, maxLines: 2, ), const SizedBox(height: AppSpacing.lg), Text( context.l10n.todoPriority, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: [ _PriorityPill( label: context.l10n.todoQuadrantImportantUrgent, selected: _priority == 1, borderColor: _colorScheme.error, activeColor: _colorScheme.error, onTap: () => setState(() => _priority = 1), ), _PriorityPill( label: context.l10n.todoQuadrantUrgentNotImportant, selected: _priority == 3, borderColor: _colorScheme.primary, activeColor: _colorScheme.primary, onTap: () => setState(() => _priority = 3), ), _PriorityPill( label: context.l10n.todoQuadrantImportantNotUrgent, selected: _priority == 2, borderColor: _colorScheme.tertiary, activeColor: _colorScheme.tertiary, onTap: () => setState(() => _priority = 2), ), ], ), ], ), ); } Widget _buildScheduleCard() { return Container( padding: const EdgeInsets.all(AppSpacing.lg), decoration: BoxDecoration( color: _colorScheme.surfaceContainerLow, borderRadius: BorderRadius.circular(AppRadius.xl), border: Border.all(color: _colorScheme.outlineVariant), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( context.l10n.todoLinkedCalendarEvents, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _colorScheme.onSurface, ), ), const Spacer(), Text( context.l10n.todoItemCount(_selectedScheduleItemIds.length), style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _colorScheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: AppSpacing.sm), if (_scheduleItems.isEmpty) Padding( padding: EdgeInsets.symmetric(vertical: AppSpacing.xl), child: Center( child: Text( context.l10n.todoNoSelectableCalendarEvents, style: TextStyle(color: _colorScheme.onSurfaceVariant), ), ), ) else ConstrainedBox( constraints: const BoxConstraints(maxHeight: 280), child: ListView.separated( shrinkWrap: true, itemCount: _scheduleItems.length, separatorBuilder: (context, index) => const SizedBox(height: AppSpacing.sm), itemBuilder: (context, index) { final item = _scheduleItems[index]; final selected = _selectedScheduleItemIds.contains(item.id); return _ScheduleSelectableTile( title: item.title, subtitle: _formatDate(item.startAt), selected: selected, onTap: () { setState(() { if (selected) { _selectedScheduleItemIds.remove(item.id); } else { _selectedScheduleItemIds.add(item.id); } }); }, ); }, ), ), ], ), ); } Widget _buildBottomAction() { final canSave = !_loading && !_saving; return Container( padding: const EdgeInsets.fromLTRB( AppSpacing.lg, AppSpacing.sm, AppSpacing.lg, AppSpacing.lg, ), decoration: BoxDecoration( color: _colorScheme.surface.withValues(alpha: 0.9), border: Border(top: BorderSide(color: _colorScheme.outlineVariant)), ), child: AppButton( text: _saving ? context.l10n.todoSaveInProgress : (widget.isCreateMode ? context.l10n.todoCreateButton : context.l10n.todoSaveChanges), onPressed: canSave ? _save : null, ), ); } Future _save() async { if (_saving) { return; } final title = _titleController.text.trim(); if (title.isEmpty) { Toast.show(context, context.l10n.todoEnterTitle, type: ToastType.warning); return; } setState(() { _saving = true; }); try { final description = _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(); if (widget.isCreateMode) { await _todoApi.createTodo( title: title, description: description, priority: _priority, scheduleItemIds: _selectedScheduleItemIds.toList(), ); } else { await _todoApi.updateTodo( widget.todoId!, title: title, description: description, priority: _priority, scheduleItemIds: _selectedScheduleItemIds.toList(), ); } if (!mounted) { return; } context.pop(true); } catch (error) { if (!mounted) { return; } Toast.show( context, context.l10n.todoSaveFailed(error.toString()), type: ToastType.error, ); } finally { if (mounted) { setState(() { _saving = false; }); } } } String _formatDate(DateTime dt) { return DateFormat.yMd(context.l10n.localeName).add_Hm().format(dt); } } class _ScheduleItemSimple { final String id; final String title; final DateTime startAt; const _ScheduleItemSimple({ required this.id, required this.title, required this.startAt, }); } class _PriorityPill extends StatelessWidget { final String label; final bool selected; final Color borderColor; final Color activeColor; final VoidCallback onTap; const _PriorityPill({ required this.label, required this.selected, required this.borderColor, required this.activeColor, required this.onTap, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return AppPressable( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.full), child: AnimatedContainer( duration: const Duration(milliseconds: 140), curve: Curves.easeOut, padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.sm, ), decoration: BoxDecoration( color: selected ? borderColor.withValues(alpha: 0.28) : colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( color: selected ? borderColor : colorScheme.outlineVariant, width: selected ? 1.5 : 1, ), ), child: Text( label, style: TextStyle( fontSize: 12, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, color: selected ? activeColor : colorScheme.onSurfaceVariant, ), ), ), ); } } class _ScheduleSelectableTile extends StatelessWidget { final String title; final String subtitle; final bool selected; final VoidCallback onTap; const _ScheduleSelectableTile({ required this.title, required this.subtitle, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return AppPressable( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.lg), child: AnimatedContainer( duration: const Duration(milliseconds: 120), curve: Curves.easeOut, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: selected ? colorScheme.primaryContainer : colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.lg), border: Border.all( color: selected ? colorScheme.primary : colorScheme.outlineVariant, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), const SizedBox(height: AppSpacing.xs), Text( subtitle, style: TextStyle( fontSize: 12, color: colorScheme.onSurfaceVariant, ), ), ], ), ), const SizedBox(width: AppSpacing.md), AnimatedContainer( duration: const Duration(milliseconds: 120), width: AppSpacing.lg, height: AppSpacing.lg, decoration: BoxDecoration( color: selected ? colorScheme.primary : colorScheme.surface, borderRadius: BorderRadius.circular(AppRadius.full), border: Border.all( color: selected ? colorScheme.primary : colorScheme.outlineVariant, ), ), child: selected ? Icon(Icons.check, size: 12, color: colorScheme.onPrimary) : null, ), ], ), ), ); } }