import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import '../models/schedule_model.dart'; import '../services/api_service.dart'; import '../theme/app_colors.dart'; import '../widgets/common/pet_selection_header.dart'; import 'schedule_form_screen.dart'; import '../providers/pet_provider.dart'; import 'pet_form_screen.dart'; class DailyCareScreen extends StatefulWidget { const DailyCareScreen({super.key}); @override State createState() => _DailyCareScreenState(); } class _DailyCareScreenState extends State { DateTime _focusedDay = DateTime.now(); DateTime _selectedDay = DateTime.now(); bool _isStampMode = false; final DraggableScrollableController _sheetController = DraggableScrollableController(); @override void dispose() { _sheetController.dispose(); super.dispose(); } // 날짜 정규화 (시간 제거) DateTime _normalizeDate(DateTime date) { return DateTime(date.year, date.month, date.day); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = context.read(); if (provider.userId == null) { provider.loadUserAndPets(); } }); } List _getSchedulesForDay( DateTime day, Map> scheduleMap, ) { return scheduleMap[_normalizeDate(day)] ?? []; } // Data Variables List _schedules = []; bool _isLoadingSchedules = false; String? _lastPetId; DateTime? _lastFocusedMonth; final ApiService _apiService = ApiService(); @override void didChangeDependencies() { super.didChangeDependencies(); _fetchSchedulesIfNeeded(); } Future _fetchSchedulesIfNeeded() async { final petProvider = context.read(); final selectedPet = petProvider.selectedPet; if (selectedPet == null) { setState(() { _schedules = []; _lastPetId = null; }); return; } bool shouldUpdate = false; // Check if Pet Changed if (_lastPetId != selectedPet.id) { shouldUpdate = true; } // Check if Month Changed (only year/month matters) if (_lastFocusedMonth == null || _lastFocusedMonth!.year != _focusedDay.year || _lastFocusedMonth!.month != _focusedDay.month) { shouldUpdate = true; } if (shouldUpdate) { _lastPetId = selectedPet.id; _lastFocusedMonth = _focusedDay; await _fetchSchedules(selectedPet.id, _focusedDay); } } Future _fetchSchedules(String petId, DateTime date) async { setState(() { _isLoadingSchedules = true; }); try { final data = await _apiService.getSchedules( petId: petId, year: date.year, month: date.month, ); final List fetchedSchedules = data .map((item) => Schedule.fromMap(item)) .toList(); if (mounted) { setState(() { _schedules = fetchedSchedules; _isLoadingSchedules = false; }); } } catch (e) { debugPrint('Error fetching schedules: $e'); if (mounted) { setState(() { _isLoadingSchedules = false; // Optional: Show error }); } } } void _onMonthChanged(DateTime focusedDay) { setState(() { _focusedDay = focusedDay; }); // Trigger fetch manually since state updated final petProvider = context.read(); if (petProvider.selectedPet != null) { _fetchSchedules(petProvider.selectedPet!.id, focusedDay); _lastFocusedMonth = focusedDay; // Update cache key } } @override Widget build(BuildContext context) { final petProvider = context.watch(); final selectedPet = petProvider.selectedPet; return Scaffold( backgroundColor: Colors.white, body: SafeArea( child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(petProvider), SizedBox(height: 10.h), _buildCustomCalendarHeader(), SizedBox(height: 30.h), Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w), child: selectedPet == null ? const Center(child: Text("반려동물을 선택해주세요")) : Builder( builder: (context) { if (_isLoadingSchedules && _schedules.isEmpty) { return const Center( child: CircularProgressIndicator(), ); } // 데이터 처리: List -> Map> Map> scheduleMap = {}; for (var schedule in _schedules) { final date = _normalizeDate(schedule.date); if (scheduleMap[date] == null) { scheduleMap[date] = []; } scheduleMap[date]!.add(schedule); } return _buildCalendar(scheduleMap); }, ), ), ), ], ), _buildBottomSheet(), ], ), ), floatingActionButton: FloatingActionButton( heroTag: 'daily_care_fab', onPressed: () { if (petProvider.selectedPet == null) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('반려동물을 먼저 선택해주세요.'))); return; } Navigator.push( context, MaterialPageRoute( builder: (context) => ScheduleFormScreen(selectedDate: _selectedDay), ), ); }, backgroundColor: AppColors.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.r), ), child: SvgPicture.asset( 'assets/icons/add_icon.svg', width: 24.w, height: 24.w, ), ), ); } Widget _buildHeader(PetProvider petProvider) { return Padding( padding: EdgeInsets.only(left: 10.w, right: 20.w, top: 6.h, bottom: 0), child: Builder( builder: (context) { final pets = petProvider.pets; final selectedPet = petProvider.selectedPet; final isLoading = petProvider.isLoading; if (isLoading) { return const SizedBox.shrink(); } return PetSelectionHeader( pets: pets, selectedPet: selectedPet, onPetSelected: (value) { context.read().selectPet(value); }, onAddPetPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => const PetFormScreen()), ).then((value) { if (value == true) { context.read().refreshPets(); } }); }, ); }, ), ); } Widget _buildCustomCalendarHeader() { return Padding( padding: EdgeInsets.symmetric(horizontal: 20.w), child: Stack( alignment: Alignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ InkWell( onTap: () { _onMonthChanged( DateTime(_focusedDay.year, _focusedDay.month - 1), ); }, child: SvgPicture.asset( 'assets/icons/left_icon.svg', width: 12.w, height: 12.w, ), ), SizedBox(width: 30.w), Text( DateFormat('yyyy년 M월', 'ko_KR').format(_focusedDay), style: TextStyle( fontFamily: 'SCDream', fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), SizedBox(width: 30.w), InkWell( onTap: () { _onMonthChanged( DateTime(_focusedDay.year, _focusedDay.month + 1), ); }, child: SvgPicture.asset( 'assets/icons/right_icon.svg', width: 12.w, height: 12.w, ), ), ], ), Positioned( right: 0, child: InkWell( onTap: () { setState(() { _isStampMode = !_isStampMode; }); }, borderRadius: BorderRadius.circular(20), child: Padding( padding: EdgeInsets.all(4.w), child: SvgPicture.asset( _isStampMode ? 'assets/icons/dashboard_icon.svg' : 'assets/icons/stamp_icon.svg', width: 24.w, height: 24.w, ), ), ), ), ], ), ); } Widget _buildCalendar(Map> scheduleMap) { return TableCalendar( shouldFillViewport: false, locale: 'ko_KR', rowHeight: 85.h, daysOfWeekHeight: 30.h, firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: _focusedDay, selectedDayPredicate: (day) => isSameDay(_selectedDay, day), headerVisible: false, onDaySelected: (selectedDay, focusedDay) { // Update selection if (!isSameDay(_selectedDay, selectedDay)) { setState(() { _selectedDay = selectedDay; }); } // Update focused month if changed (triggers stream update) if (focusedDay.month != _focusedDay.month || focusedDay.year != _focusedDay.year) { _onMonthChanged(focusedDay); } else { // Just update focused day references without stream update if (!isSameDay(_focusedDay, focusedDay)) { setState(() { _focusedDay = focusedDay; }); } } }, onPageChanged: (focusedDay) { if (focusedDay.month != _focusedDay.month || focusedDay.year != _focusedDay.year) { _onMonthChanged(focusedDay); } }, calendarStyle: CalendarStyle( defaultTextStyle: TextStyle( fontFamily: 'SCDream', fontWeight: FontWeight.w500, fontSize: 15.sp, color: const Color(0xFF1f1f1f), ), weekendTextStyle: TextStyle( fontFamily: 'SCDream', fontWeight: FontWeight.w500, fontSize: 15.sp, color: const Color(0xFF1f1f1f), ), todayDecoration: const BoxDecoration( color: Colors.transparent, shape: BoxShape.circle, ), todayTextStyle: const TextStyle( color: AppColors.highlight, fontWeight: FontWeight.bold, ), selectedDecoration: const BoxDecoration( color: Colors.transparent, shape: BoxShape.circle, ), selectedTextStyle: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, ), outsideDaysVisible: true, ), eventLoader: (day) => _getSchedulesForDay(day, scheduleMap), calendarBuilders: CalendarBuilders( markerBuilder: (context, day, events) { if (events.isEmpty) return null; bool isOutside = day.month != _focusedDay.month; double opacity = isOutside ? 0.3 : 1.0; if (_isStampMode) { int total = events.length; int successCount = events.where((s) => s.isCompleted).length; bool isGood = (successCount / total) >= 0.5; return Positioned( top: 40.h, left: 0, right: 0, child: Center( child: Opacity( opacity: opacity, child: Image.asset( isGood ? 'assets/img/good.png' : 'assets/img/bad.png', width: 24.w, height: 24.w, fit: BoxFit.contain, ), ), ), ); } return Positioned( top: 32.h, left: 0, right: 0, child: Opacity( opacity: opacity, child: Center( child: Container( width: 40.w, child: Wrap( alignment: WrapAlignment.start, spacing: 2.w, runSpacing: 2.h, children: events.take(9).map((schedule) { String iconPath = 'assets/icons/general_schedule_icon.svg'; // 상태/타입에 따른 아이콘 결정 if (schedule.isCompleted) { iconPath = 'assets/icons/flower_icon.svg'; } else { if (schedule.type == 'important') { iconPath = 'assets/icons/important_schedule_icon.svg'; } else { // 일반 일정 미완료 iconPath = 'assets/icons/incomplete_icon.svg'; } } return SvgPicture.asset( iconPath, width: 12.w, height: 12.w, ); }).toList(), ), ), ), ), ); }, dowBuilder: (context, day) { final text = DateFormat.E('ko_KR').format(day); Color color = const Color(0xFF939393); if (day.weekday == DateTime.sunday) color = const Color(0xFFFF3F3F); return Container( padding: EdgeInsets.only(bottom: 10.h), alignment: Alignment.bottomCenter, child: Text( text, style: TextStyle( color: color, fontFamily: 'SCDream', fontSize: 12.sp, fontWeight: FontWeight.w500, ), ), ); }, outsideBuilder: (context, day, focusedDay) { Color color = const Color(0xFF1F1F1F).withOpacity(0.5); if (day.weekday == DateTime.sunday) { color = const Color(0xFFFF3F3F).withOpacity(0.5); } return _buildDayCell(day, color); }, defaultBuilder: (context, day, focusedDay) { Color color = const Color(0xFF1F1F1F); if (day.weekday == DateTime.sunday) { color = const Color(0xFFFF3F3F); } return _buildDayCell(day, color); }, prioritizedBuilder: (context, day, focusedDay) { final isToday = isSameDay(day, DateTime.now()); final isSelected = isSameDay(day, _selectedDay); if (!isToday && !isSelected) return null; final isSunday = day.weekday == DateTime.sunday; Color textColor = const Color(0xFF1F1F1F); if (isToday) { textColor = const Color(0xFFFF9500); } else if (isSunday) { textColor = const Color(0xFFFF3F3F); } BoxDecoration? innerDecoration; if (isSelected) { innerDecoration = BoxDecoration( color: const Color(0xFFFFEDBC), borderRadius: BorderRadius.circular(6.r), ); } return Container( decoration: const BoxDecoration( border: Border( top: BorderSide(color: Color(0xFFE0E0E0), width: 1), ), ), child: Container( margin: EdgeInsets.symmetric(vertical: 2.h), decoration: innerDecoration, alignment: Alignment.topCenter, padding: EdgeInsets.only(top: 6.h), child: Text( day.day.toString().padLeft(2, '0'), style: TextStyle( fontFamily: 'SCDream', fontSize: 15.sp, fontWeight: FontWeight.bold, color: textColor, ), ), ), ); }, ), ); } Widget _buildDayCell(DateTime day, Color textColor) { return Container( decoration: const BoxDecoration( border: Border(top: BorderSide(color: Color(0xFFE0E0E0), width: 1)), ), padding: EdgeInsets.only(top: 8.h), alignment: Alignment.topCenter, child: Text( day.day.toString().padLeft(2, '0'), style: TextStyle( fontFamily: 'SCDream', fontSize: 15.sp, fontWeight: FontWeight.w500, color: textColor, ), ), ); } Widget _buildBottomSheet() { // 임시로 비워둠 (실제 리스트 구현 시 채울 예정) // 현재는 일정 추가만 구현 return DraggableScrollableSheet( controller: _sheetController, initialChildSize: 0.08, minChildSize: 0.08, maxChildSize: 0.8, builder: (context, scrollController) { return Container( margin: EdgeInsets.symmetric(horizontal: 20.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 5, offset: const Offset(0, -3), ), ], ), child: SingleChildScrollView( controller: scrollController, child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), child: Column( children: [ GestureDetector( onTap: () { if (_sheetController.size > 0.4) { _sheetController.animateTo( 0.08, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } else { _sheetController.animateTo( 0.8, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } }, child: Container( color: Colors.transparent, width: double.infinity, child: Column( children: [ Icon( Icons.keyboard_arrow_up_rounded, color: Colors.grey[400], size: 24.w, ), SizedBox(height: 20.h), ], ), ), ), Align( alignment: Alignment.centerLeft, child: Text( '일정 리스트 준비 중...', // TODO: Implement list style: TextStyle( fontFamily: 'SCDream', fontSize: 14.sp, color: Colors.grey[600], ), ), ), SizedBox(height: 50.h), ], ), ), ), ); }, ); } }