import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../../data/models/maintenance_dto.dart'; import 'package:superport/screens/common/theme_shadcn.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: ShadcnTheme.card, boxShadow: ShadcnTheme.shadowSm, ), 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: ShadcnTheme.labelMedium.copyWith( fontWeight: FontWeight.w600, color: i == 0 ? ShadcnTheme.destructive : (i == 6 ? ShadcnTheme.accent : ShadcnTheme.foreground), ), ), ), ); } // 이전 달 빈 칸 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 ? ShadcnTheme.primary.withValues(alpha: 0.12) : isToday ? ShadcnTheme.accent.withValues(alpha: 0.08) : Colors.transparent, border: Border.all( color: isSelected ? ShadcnTheme.primary : isToday ? ShadcnTheme.accent : Colors.transparent, width: isSelected || isToday ? 2 : 1, ), borderRadius: BorderRadius.circular(8), ), child: Stack( children: [ // 날짜 숫자 Positioned( top: 8, left: 8, child: Text( day.toString(), style: ShadcnTheme.bodySmall.copyWith( fontWeight: isToday ? FontWeight.w700 : FontWeight.w400, color: isWeekend ? (date.weekday == DateTime.sunday ? ShadcnTheme.destructive : ShadcnTheme.accent) : ShadcnTheme.foreground, ), ), ), // 유지보수 표시 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: ShadcnTheme.caption.copyWith( color: ShadcnTheme.primaryForeground, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ); }).toList(), ), ), // 더 많은 일정이 있을 때 if (dayMaintenances.length > 3) Positioned( bottom: 2, right: 2, child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: ShadcnTheme.secondaryDark, shape: BoxShape.circle, ), child: Text( '+${dayMaintenances.length - 3}', style: ShadcnTheme.caption.copyWith( fontSize: 9, color: ShadcnTheme.primaryForeground, ), ), ), ), ], ), ), ), ); } 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 ShadcnTheme.alertExpired; case 'scheduled': case 'upcoming': final daysUntil = maintenance.startedAt.difference(DateTime.now()).inDays; if (daysUntil <= 7) { return ShadcnTheme.alertWarning30; } else if (daysUntil <= 30) { return ShadcnTheme.alertWarning60; } return ShadcnTheme.info; case 'inprogress': case 'ongoing': return ShadcnTheme.purple; case 'completed': return ShadcnTheme.success; default: return ShadcnTheme.secondary; } } }