import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:social_app/core/l10n/l10n.dart'; import '../../../../app/di/injection.dart'; import '../../../../app/router/app_routes.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/app_selection_sheet.dart'; import '../../../../shared/widgets/app_sheet_input_field.dart'; import '../../../../shared/widgets/back_title_page_header.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import 'date_time_picker_sheet.dart'; import '../../../../features/calendar/data/models/schedule_item_model.dart'; import '../../../../features/calendar/data/services/calendar_service.dart'; final _defaultColors = AppColorPalette.light.eventPresetColors; class CreateEventSheet extends StatefulWidget { final DateTime? initialDate; final ScheduleItemModel? editingEvent; final VoidCallback? onSaved; final bool pageMode; const CreateEventSheet({ super.key, this.initialDate, this.editingEvent, this.onSaved, this.pageMode = false, }); static Future show( BuildContext context, { DateTime? initialDate, VoidCallback? onSaved, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Theme.of( context, ).colorScheme.surface.withValues(alpha: 0), builder: (context) => CreateEventSheet(initialDate: initialDate, onSaved: onSaved), ); } static Future edit( BuildContext context, ScheduleItemModel event, { VoidCallback? onSaved, }) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Theme.of( context, ).colorScheme.surface.withValues(alpha: 0), builder: (context) => CreateEventSheet(editingEvent: event, onSaved: onSaved), ); } @override State createState() => _CreateEventSheetState(); } class _CreateEventSheetState extends State with SingleTickerProviderStateMixin { static const List _defaultReminderOptions = [ null, 0, 5, 10, 15, 30, 60, 120, ]; late TabController _tabController; final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _locationController = TextEditingController(); final _notesController = TextEditingController(); late DateTime _startDate; late DateTime _startTime; DateTime? _endDate; DateTime? _endTime; String _selectedColor = '#3B82F6'; int? _reminderMinutes = 15; bool _saving = false; bool get _isEditing => widget.editingEvent != null; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); if (_isEditing) { final event = widget.editingEvent!; _titleController.text = event.title; _descriptionController.text = event.description ?? ''; _locationController.text = event.metadata?.location ?? ''; _notesController.text = event.metadata?.notes ?? ''; _startDate = event.startAt; _startTime = event.startAt; _endDate = event.endAt; _endTime = event.endAt; _selectedColor = event.metadata?.color ?? '#3B82F6'; _reminderMinutes = _sanitizeReminderMinutes( event.metadata?.reminderMinutes, ); } else { final now = DateTime.now(); final initial = widget.initialDate; final rounded = _roundToNearestMinute(now, 5); _startDate = initial != null ? DateTime( initial.year, initial.month, initial.day, rounded.hour, rounded.minute, ) : rounded; _startTime = _startDate; _endDate = _startDate; _endTime = _startDate.add(const Duration(hours: 1)); } } @override void dispose() { _tabController.dispose(); _titleController.dispose(); _descriptionController.dispose(); _locationController.dispose(); _notesController.dispose(); super.dispose(); } DateTime _roundToNearestMinute(DateTime dt, int interval) { final totalMinutes = dt.hour * 60 + dt.minute; final rounded = ((totalMinutes / interval).round() * interval); final hours = rounded ~/ 60; final minutes = rounded % 60; final dayOffset = hours >= 24 ? 1 : 0; final newHour = hours % 24; return DateTime(dt.year, dt.month, dt.day + dayOffset, newHour, minutes); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; if (widget.pageMode) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => FocusScope.of(context).unfocus(), child: Container( color: colorScheme.surface, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildPageHeader(), _buildTabBar(), Expanded(child: _buildTabContent()), ], ), ), ); } return AnimatedPadding( duration: const Duration(milliseconds: 150), curve: Curves.easeOut, padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( height: MediaQuery.of(context).size.height * 0.85, decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: AppSpacing.sm), Center( child: Container( width: AppSpacing.xl + AppSpacing.sm, height: AppSpacing.xs, decoration: BoxDecoration( color: colorScheme.outlineVariant, borderRadius: BorderRadius.circular(AppRadius.full), ), ), ), const SizedBox(height: AppSpacing.sm), _buildHeader(), _buildTabBar(), Expanded(child: _buildTabContent()), ], ), ), ); } Widget _buildPageHeader() { final colorScheme = Theme.of(context).colorScheme; return BackTitlePageHeader( title: _isEditing ? context.l10n.calendarCreateEditTitle : context.l10n.calendarCreateNewTitle, onBack: () => _closeSheet(), trailing: ValueListenableBuilder( valueListenable: _titleController, builder: (context, value, child) { final enabled = value.text.trim().isNotEmpty && !_saving; return SizedBox( height: AppSpacing.xxl * 2, child: TextButton( onPressed: enabled ? _saveEvent : null, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), minimumSize: const Size(AppSpacing.none, AppSpacing.none), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: _saving ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, trackColor: null, ) : Text( context.l10n.commonSave, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: enabled ? colorScheme.primary : colorScheme.outline, ), ), ), ); }, ), ); } Widget _buildHeader() { final colorScheme = Theme.of(context).colorScheme; return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( width: AppSpacing.xxl * 2, height: AppSpacing.xxl * 2, child: TextButton( onPressed: _closeSheet, style: TextButton.styleFrom( padding: const EdgeInsets.all(AppSpacing.none), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.full), ), ), child: Icon( LucideIcons.x, size: AppSpacing.xxl, color: colorScheme.onSurfaceVariant, ), ), ), Text( _isEditing ? context.l10n.calendarCreateEditTitle : context.l10n.calendarCreateNewTitle, style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: colorScheme.onSurface, ), ), ValueListenableBuilder( valueListenable: _titleController, builder: (context, value, child) { final enabled = value.text.trim().isNotEmpty && !_saving; return SizedBox( height: AppSpacing.xxl * 2, child: TextButton( onPressed: enabled ? _saveEvent : null, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, ), minimumSize: const Size(AppSpacing.none, AppSpacing.none), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: _saving ? const AppLoadingIndicator( variant: AppLoadingVariant.button, size: 18, trackColor: null, ) : Text( context.l10n.commonSave, style: TextStyle( fontSize: 17, fontWeight: FontWeight.w600, color: enabled ? colorScheme.primary : colorScheme.outline, ), ), ), ); }, ), ], ), ); } Widget _buildTabBar() { final colorScheme = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( border: Border(bottom: BorderSide(color: colorScheme.outlineVariant)), ), child: TabBar( controller: _tabController, labelColor: colorScheme.primary, unselectedLabelColor: colorScheme.onSurfaceVariant, indicatorColor: colorScheme.primary, tabs: [ Tab(text: context.l10n.calendarCreateTabBasic), Tab(text: context.l10n.calendarCreateTabAdvanced), ], ), ); } Widget _buildTabContent() { return TabBarView( controller: _tabController, children: [_buildBasicTab(), _buildAdvancedTab()], ); } Widget _buildBasicTab() { return SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField( context.l10n.calendarCreateFieldTitle, _titleController, context.l10n.calendarCreateFieldTitleHint, ), const SizedBox(height: 20), _buildDateTimePicker( context.l10n.calendarCreateFieldStart, _startDate, _startTime, (date, time) { setState(() { _startDate = date; _startTime = time; if (_endDate != null && _endTime != null) { final endDateTime = _composeDateTime(_endDate!, _endTime!); final startDateTime = _composeDateTime(date, time); if (endDateTime.isBefore(startDateTime) || endDateTime.isAtSameMomentAs(startDateTime)) { _setEndDateTime(_defaultEndDateTime(startDateTime)); } } }); }, ), const SizedBox(height: 20), _buildDateTimePicker( context.l10n.calendarCreateFieldEnd, _endDate ?? _startDate, _endTime ?? _startTime, (date, time) { setState(() { final startDateTime = _composeDateTime(_startDate, _startTime); final endDateTime = _composeDateTime(date, time); if (endDateTime.isBefore(startDateTime) || endDateTime.isAtSameMomentAs(startDateTime)) { Toast.show( context, context.l10n.calendarCreateInvalidTimeRange, type: ToastType.error, ); _setEndDateTime(_defaultEndDateTime(startDateTime)); } else { _setEndDateTime(endDateTime); } }); }, isOptional: true, ), ], ), ); } DateTime _composeDateTime(DateTime date, DateTime time) { return DateTime(date.year, date.month, date.day, time.hour, time.minute); } DateTime _defaultEndDateTime(DateTime startDateTime) { return startDateTime.add(const Duration(hours: 1)); } void _setEndDateTime(DateTime value) { _endDate = DateTime(value.year, value.month, value.day); _endTime = DateTime( value.year, value.month, value.day, value.hour, value.minute, ); } Widget _buildAdvancedTab() { return SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTextField( context.l10n.calendarCreateFieldDescription, _descriptionController, context.l10n.calendarCreateFieldDescriptionHint, ), const SizedBox(height: 20), _buildTextField( context.l10n.calendarCreateFieldLocation, _locationController, context.l10n.calendarCreateFieldLocationHint, ), const SizedBox(height: 20), _buildReminderPicker(), const SizedBox(height: 20), _buildColorPicker(), const SizedBox(height: 20), _buildTextField( context.l10n.calendarDetailNotes, _notesController, context.l10n.calendarCreateFieldNotesHint, maxLines: 3, ), ], ), ); } Widget _buildTextField( String label, TextEditingController controller, String hint, { int maxLines = 1, bool autofocus = false, }) { return AppSheetInputField( controller: controller, label: label, hint: hint, maxLines: maxLines, autofocus: autofocus, ); } Widget _buildDateTimePicker( String label, DateTime date, DateTime time, Function(DateTime, DateTime) onChanged, { bool isOptional = false, DateTime? minTime, }) { final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isOptional ? context.l10n.calendarCreateOptionalField(label) : label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), InkWell( onTap: () async { final picked = await _pickDateTime(date, time, minTime: minTime); if (picked == null) { return; } onChanged(picked.$1, picked.$2); }, borderRadius: BorderRadius.circular(AppRadius.md), child: Container( padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( LucideIcons.calendar, size: 16, color: colorScheme.onSurfaceVariant, ), const SizedBox(width: AppSpacing.sm), Expanded( child: Text( _formatDateTimeLabel(date, time), style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: colorScheme.onSurface, ), ), ), Icon( LucideIcons.chevronRight, size: 16, color: colorScheme.outline, ), ], ), ), ), ], ); } String _formatDateTimeLabel(DateTime date, DateTime time) { return context.l10n.calendarCreateDateTimeLabel( date.year, date.month, date.day, time.hour.toString().padLeft(2, '0'), time.minute.toString().padLeft(2, '0'), ); } Future<(DateTime, DateTime)?> _pickDateTime( DateTime date, DateTime time, { DateTime? minTime, }) async { final result = await showModalBottomSheet<(DateTime, DateTime)>( context: context, backgroundColor: Theme.of( context, ).colorScheme.surface.withValues(alpha: 0), isScrollControlled: true, builder: (context) => DateTimePickerSheet( initialDate: date, initialTime: time, minTime: minTime, ), ); return result; } Widget _buildColorPicker() { final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.calendarDetailColor, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), Row( children: _defaultColors.map((color) { final colorHex = '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}'; final isSelected = _selectedColor == colorHex; final checkColor = ThemeData.estimateBrightnessForColor(color) == Brightness.dark ? Colors.white : Colors.black; return GestureDetector( onTap: () => setState(() => _selectedColor = colorHex), child: Container( margin: const EdgeInsets.only(right: 12), width: 32, height: 32, decoration: BoxDecoration( color: color, shape: BoxShape.circle, border: isSelected ? Border.all(color: colorScheme.onSurface, width: 2) : null, ), child: isSelected ? Icon(Icons.check, size: 16, color: checkColor) : null, ), ); }).toList(), ), ], ); } Widget _buildReminderPicker() { final colorScheme = Theme.of(context).colorScheme; String labelOf(int? value) { if (value == null) { return context.l10n.calendarCreateReminderNone; } if (value == 0) { return context.l10n.calendarDetailReminderOnTime; } return context.l10n.calendarDetailReminderBeforeMinutes(value); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( context.l10n.calendarCreateReminderTime, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), InkWell( onTap: () async { final options = _buildReminderOptions(); final selected = await showAppSelectionSheet( context, title: context.l10n.calendarCreatePickReminderTime, items: options .map((v) => AppSelectionItem(value: v, label: labelOf(v))) .toList(), selectedValue: _reminderMinutes, ); if (selected != null) { setState(() { _reminderMinutes = selected; }); } }, borderRadius: BorderRadius.circular(AppRadius.md), child: Container( width: double.infinity, padding: const EdgeInsets.all(AppSpacing.md), decoration: BoxDecoration( color: colorScheme.surfaceContainerLowest, borderRadius: BorderRadius.circular(AppRadius.md), border: Border.all(color: colorScheme.outlineVariant), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Text( labelOf(_reminderMinutes), style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: colorScheme.onSurface, ), ), ), Icon( LucideIcons.chevronRight, size: 16, color: colorScheme.outline, ), ], ), ), ), ], ); } int? _sanitizeReminderMinutes(int? minutes) { return (minutes != null && minutes >= 0) ? minutes : null; } List _buildReminderOptions() { final current = _sanitizeReminderMinutes(_reminderMinutes); final nonNull = _defaultReminderOptions.whereType().toSet(); if (current != null) { nonNull.add(current); } final sorted = nonNull.toList()..sort(); return [null, ...sorted]; } Future _saveEvent() async { if (_titleController.text.trim().isEmpty || _saving) return; setState(() { _saving = true; }); final startAt = DateTime( _startDate.year, _startDate.month, _startDate.day, _startTime.hour, _startTime.minute, ); DateTime? endAt; if (_endDate != null && _endTime != null) { endAt = DateTime( _endDate!.year, _endDate!.month, _endDate!.day, _endTime!.hour, _endTime!.minute, ); } final metadata = ScheduleMetadata( color: _selectedColor, location: _locationController.text.trim().isNotEmpty ? _locationController.text.trim() : null, notes: _notesController.text.trim().isNotEmpty ? _notesController.text.trim() : null, reminderMinutes: _reminderMinutes, attachments: const [], version: widget.editingEvent?.metadata?.version ?? 1, ); final event = ScheduleItemModel( id: _isEditing ? widget.editingEvent!.id : 'evt_${DateTime.now().millisecondsSinceEpoch}', ownerId: widget.editingEvent?.ownerId ?? '', title: _titleController.text.trim(), description: _descriptionController.text.trim().isNotEmpty ? _descriptionController.text.trim() : null, startAt: startAt, endAt: endAt, metadata: metadata, ); var saved = false; try { final service = sl(); if (_isEditing) { await service.updateEvent(event); } else { await service.addEvent(event); } saved = true; widget.onSaved?.call(); } catch (e) { if (mounted) { Toast.show( context, context.l10n.todoSaveFailed('$e'), type: ToastType.error, ); } } finally { if (mounted) { setState(() { _saving = false; }); } } if (saved && mounted) { _closeSheet(result: true); } } void _closeSheet({Object? result}) { if (!mounted) { return; } final navigator = Navigator.of(context); if (navigator.canPop()) { navigator.pop(result); return; } final router = GoRouter.of(context); if (router.canPop()) { context.pop(result); return; } context.go(AppRoutes.homeMain); } }