import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../../data/models/maintenance_dto.dart'; class MaintenanceCalendar extends StatefulWidget { final List maintenances; final Function(DateTime)? onDateSelected; const MaintenanceCalendar({ super.key, required this.maintenances, this.onDateSelected, }); @override State createState() => _MaintenanceCalendarState(); } class _MaintenanceCalendarState extends State { late DateTime _selectedMonth; DateTime? _selectedDate; @override void initState() { super.initState(); _selectedMonth = DateTime.now(); } @override Widget build(BuildContext context) { return Column( children: [ _buildMonthSelector(), Expanded( child: _buildCalendarGrid(), ), ], ); } Widget _buildMonthSelector() { return Container( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.chevron_left), onPressed: () { setState(() { _selectedMonth = DateTime( _selectedMonth.year, _selectedMonth.month - 1, ); }); }, ), Text( DateFormat('yyyy년 MM월').format(_selectedMonth), style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), IconButton( icon: const Icon(Icons.chevron_right), onPressed: () { setState(() { _selectedMonth = DateTime( _selectedMonth.year, _selectedMonth.month + 1, ); }); }, ), ], ), ); } Widget _buildCalendarGrid() { final firstDay = DateTime(_selectedMonth.year, _selectedMonth.month, 1); final lastDay = DateTime(_selectedMonth.year, _selectedMonth.month + 1, 0); final startWeekday = firstDay.weekday % 7; // 일요일 = 0 // 달력 셀 생성 final days = []; // 요일 헤더 const weekdays = ['일', '월', '화', '수', '목', '금', '토']; for (int i = 0; i < 7; i++) { days.add( Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 12), child: Text( weekdays[i], style: TextStyle( fontWeight: FontWeight.bold, color: i == 0 ? Colors.red : (i == 6 ? Colors.blue : Colors.black), ), ), ), ); } // 이전 달 빈 칸 for (int i = 0; i < startWeekday; i++) { days.add(Container()); } // 현재 달 날짜 for (int day = 1; day <= lastDay.day; day++) { final date = DateTime(_selectedMonth.year, _selectedMonth.month, day); final dayMaintenances = _getMaintenancesForDate(date); final isToday = _isToday(date); final isSelected = _selectedDate != null && _isSameDay(date, _selectedDate!); final isWeekend = date.weekday == DateTime.sunday || date.weekday == DateTime.saturday; days.add( InkWell( onTap: () { setState(() { _selectedDate = date; }); if (widget.onDateSelected != null && dayMaintenances.isNotEmpty) { widget.onDateSelected!(date); } }, child: Container( margin: const EdgeInsets.all(2), decoration: BoxDecoration( color: isSelected ? Theme.of(context).primaryColor.withValues(alpha: 0.2) : isToday ? Colors.blue.withValues(alpha: 0.1) : Colors.transparent, border: Border.all( color: isSelected ? Theme.of(context).primaryColor : isToday ? Colors.blue : Colors.transparent, width: isSelected || isToday ? 2 : 1, ), borderRadius: BorderRadius.circular(8), ), child: Stack( children: [ // 날짜 숫자 Positioned( top: 8, left: 8, child: Text( day.toString(), style: TextStyle( fontWeight: isToday ? FontWeight.bold : FontWeight.normal, color: isWeekend ? (date.weekday == DateTime.sunday ? Colors.red : Colors.blue) : Colors.black, ), ), ), // 유지보수 표시 if (dayMaintenances.isNotEmpty) Positioned( bottom: 4, left: 4, right: 4, child: Column( children: dayMaintenances.take(3).map((m) { return Container( margin: const EdgeInsets.symmetric(vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: _getMaintenanceColor(m), borderRadius: BorderRadius.circular(4), ), child: Text( '#${m.equipmentHistoryId}', style: const TextStyle( fontSize: 10, color: Colors.white, ), overflow: TextOverflow.ellipsis, ), ); }).toList(), ), ), // 더 많은 일정이 있을 때 if (dayMaintenances.length > 3) Positioned( bottom: 2, right: 2, child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: Colors.grey[600], shape: BoxShape.circle, ), child: Text( '+${dayMaintenances.length - 3}', style: const TextStyle( fontSize: 8, color: Colors.white, ), ), ), ), ], ), ), ), ); } return Container( padding: const EdgeInsets.all(16), child: GridView.count( crossAxisCount: 7, childAspectRatio: 1.2, children: days, ), ); } List _getMaintenancesForDate(DateTime date) { return widget.maintenances.where((m) { // nextMaintenanceDate 필드가 백엔드에 없으므로 startedAt~endedAt 기간으로 확인 final targetDate = DateTime(date.year, date.month, date.day); final startDate = DateTime(m.startedAt.year, m.startedAt.month, m.startedAt.day); final endDate = DateTime(m.endedAt.year, m.endedAt.month, m.endedAt.day); return (targetDate.isAfter(startDate) || targetDate.isAtSameMomentAs(startDate)) && (targetDate.isBefore(endDate) || targetDate.isAtSameMomentAs(endDate)); }).toList(); } bool _isSameDay(DateTime date1, DateTime date2) { return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day; } bool _isToday(DateTime date) { final now = DateTime.now(); return _isSameDay(date, now); } Color _getMaintenanceColor(MaintenanceDto maintenance) { // status 필드가 백엔드에 없으므로 날짜 기반으로 상태 계산 final now = DateTime.now(); String status; if (maintenance.isDeleted ?? false) { status = 'cancelled'; } else if (maintenance.startedAt.isAfter(now)) { status = 'scheduled'; } else if (maintenance.endedAt.isBefore(now)) { status = 'overdue'; } else { status = 'in_progress'; } switch (status.toLowerCase()) { case 'overdue': return Colors.red; case 'scheduled': case 'upcoming': // nextMaintenanceDate 필드가 없으므로 startedAt 기반으로 계산 final daysUntil = maintenance.startedAt.difference(DateTime.now()).inDays; if (daysUntil <= 7) { return Colors.orange; } else if (daysUntil <= 30) { return Colors.yellow[700]!; } return Colors.blue; case 'inprogress': case 'ongoing': return Colors.purple; case 'completed': return Colors.green; default: return Colors.grey; } } }