rup-project/app/lib/screens/daily_care_screen.dart

658 lines
20 KiB
Dart

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<DailyCareScreen> createState() => _DailyCareScreenState();
}
class _DailyCareScreenState extends State<DailyCareScreen> {
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<PetProvider>();
if (provider.userId == null) {
provider.loadUserAndPets();
}
});
}
List<Schedule> _getSchedulesForDay(
DateTime day,
Map<DateTime, List<Schedule>> scheduleMap,
) {
return scheduleMap[_normalizeDate(day)] ?? [];
}
// Data Variables
List<Schedule> _schedules = [];
bool _isLoadingSchedules = false;
String? _lastPetId;
DateTime? _lastFocusedMonth;
final ApiService _apiService = ApiService();
@override
void didChangeDependencies() {
super.didChangeDependencies();
_fetchSchedulesIfNeeded();
}
Future<void> _fetchSchedulesIfNeeded() async {
final petProvider = context.read<PetProvider>();
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<void> _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<Schedule> 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<PetProvider>();
if (petProvider.selectedPet != null) {
_fetchSchedules(petProvider.selectedPet!.id, focusedDay);
_lastFocusedMonth = focusedDay; // Update cache key
}
}
@override
Widget build(BuildContext context) {
final petProvider = context.watch<PetProvider>();
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<Schedule> -> Map<DateTime, List<Schedule>>
Map<DateTime, List<Schedule>> 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<PetProvider>().selectPet(value);
},
onAddPetPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PetFormScreen()),
).then((value) {
if (value == true) {
context.read<PetProvider>().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<DateTime, List<Schedule>> scheduleMap) {
return TableCalendar<Schedule>(
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),
],
),
),
),
);
},
);
}
}