import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/maintenance/controllers/maintenance_dashboard_controller.dart'; import 'package:superport/screens/maintenance/controllers/maintenance_controller.dart'; import 'package:superport/screens/maintenance/widgets/status_summary_cards.dart'; import 'package:superport/screens/maintenance/maintenance_form_dialog.dart'; import 'package:superport/data/models/maintenance_dto.dart'; import 'package:superport/screens/common/widgets/standard_data_table.dart'; /// 유지보수 대시보드 화면 (Phase 9.2) /// StatusSummaryCards + 필터링된 유지보수 목록으로 구성 /// 100% shadcn_ui 컴플라이언스 + Clean Architecture 패턴 class MaintenanceAlertDashboard extends StatefulWidget { const MaintenanceAlertDashboard({super.key}); @override State createState() => _MaintenanceAlertDashboardState(); } class _MaintenanceAlertDashboardState extends State { String _activeFilter = 'all'; // all, expiring_60, expiring_30, expiring_7, expired @override void initState() { super.initState(); // 대시보드 데이터 로드 WidgetsBinding.instance.addPostFrameCallback((_) { final dashboardController = context.read(); final maintenanceController = context.read(); dashboardController.loadDashboardStats(); maintenanceController.loadAlerts(); maintenanceController.loadMaintenances(refresh: true); }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: ShadcnTheme.background, appBar: _buildAppBar(), body: Consumer2( builder: (context, dashboardController, maintenanceController, child) { return RefreshIndicator( onRefresh: () async { await dashboardController.refreshDashboardStats(); await maintenanceController.loadAlerts(); }, child: SingleChildScrollView( padding: const EdgeInsets.all(24), physics: const AlwaysScrollableScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 상단 통계 카드 (핵심 기능) _buildStatisticsCards(dashboardController), const SizedBox(height: 32), // 필터 탭 _buildFilterTabs(), const SizedBox(height: 24), // 필터링된 유지보수 목록 _buildFilteredMaintenanceList(maintenanceController), const SizedBox(height: 32), // 빠른 작업 버튼들 _buildQuickActions(), ], ), ), ); }, ), ); } /// 앱바 구성 PreferredSizeWidget _buildAppBar() { return AppBar( title: Text( '유지보수 대시보드', style: ShadcnTheme.headingH2.copyWith( fontWeight: FontWeight.w600, ), ), backgroundColor: ShadcnTheme.background, elevation: 0, actions: [ ShadButton.ghost( onPressed: () => _refreshData(), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.refresh, size: 18), SizedBox(width: 8), Text('새로고침'), ], ), ), const SizedBox(width: 16), ], ); } /// 통계 카드 섹션 (StatusSummaryCards 사용) Widget _buildStatisticsCards(MaintenanceDashboardController controller) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '유지보수 현황', style: ShadcnTheme.headingH3.copyWith( fontWeight: FontWeight.w600, ), ), if (controller.lastUpdated != null) Text( '최종 업데이트: ${controller.timeSinceLastUpdate}', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.mutedForeground, ), ), ], ), const SizedBox(height: 16), // StatusSummaryCards 컴포넌트 (반응형 지원) LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth > 768) { // 데스크톱: 가로 4개 카드 return StatusSummaryCards( stats: controller.stats, isLoading: controller.isLoading, error: controller.errorMessage, onRetry: controller.retry, onCardTap: _handleCardTap, ); } else { // 태블릿/모바일: 2x2 그리드 return _buildMobileCards(controller); } }, ), ], ); } /// 모바일용 2x2 그리드 카드 Widget _buildMobileCards(MaintenanceDashboardController controller) { final cardData = [ {'type': 'expiring_60', 'title': '60일 내', 'count': controller.stats.expiring60Days}, {'type': 'expiring_30', 'title': '30일 내', 'count': controller.stats.expiring30Days}, {'type': 'expiring_7', 'title': '7일 내', 'count': controller.stats.expiring7Days}, {'type': 'expired', 'title': '만료됨', 'count': controller.stats.expiredContracts}, ]; return Column( children: [ Row( children: [ Expanded(child: _buildMobileCard(cardData[0])), const SizedBox(width: 16), Expanded(child: _buildMobileCard(cardData[1])), ], ), const SizedBox(height: 16), Row( children: [ Expanded(child: _buildMobileCard(cardData[2])), const SizedBox(width: 16), Expanded(child: _buildMobileCard(cardData[3])), ], ), ], ); } /// 모바일 카드 개별 구성 Widget _buildMobileCard(Map cardData) { final type = cardData['type'] as String; final title = cardData['title'] as String; final count = cardData['count'] as int; Color color; IconData icon; // Phase 10: 색체심리학 기반 알림 색상 체계 적용 switch (type) { case 'expiring_7': color = ShadcnTheme.alertCritical7; // 7일 이내 - 위험 (레드) icon = Icons.priority_high_outlined; break; case 'expiring_30': color = ShadcnTheme.alertWarning30; // 30일 이내 - 경고 (오렌지) icon = Icons.warning_amber_outlined; break; case 'expiring_60': color = ShadcnTheme.alertWarning60; // 60일 이내 - 주의 (앰버) icon = Icons.schedule_outlined; break; case 'expired': color = ShadcnTheme.alertExpired; // 만료됨 - 심각 (진한 레드) icon = Icons.error_outline; break; default: color = ShadcnTheme.alertNormal; // 정상 - 안전 (그린) icon = Icons.info_outline; } return ShadCard( child: InkWell( onTap: () => _handleCardTap(type), borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 24, color: color), const SizedBox(height: 8), Text( title, style: ShadcnTheme.bodyMedium.copyWith( color: ShadcnTheme.mutedForeground, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 4), Text( count.toString(), style: ShadcnTheme.headingH2.copyWith( color: color, fontWeight: FontWeight.bold, ), ), ], ), ), ), ); } /// 필터 탭 Widget _buildFilterTabs() { final filters = [ {'key': 'all', 'label': '전체', 'icon': Icons.list_outlined}, {'key': 'expiring_7', 'label': '7일 내', 'icon': Icons.priority_high_outlined}, {'key': 'expiring_30', 'label': '30일 내', 'icon': Icons.warning_amber_outlined}, {'key': 'expiring_60', 'label': '60일 내', 'icon': Icons.schedule_outlined}, {'key': 'expired', 'label': '만료됨', 'icon': Icons.error_outline}, ]; return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: filters.map((filter) { final isActive = _activeFilter == filter['key']; return Padding( padding: const EdgeInsets.only(right: 12), child: isActive ? ShadButton( onPressed: () => setState(() => _activeFilter = filter['key'] as String), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(filter['icon'] as IconData, size: 16), const SizedBox(width: 6), Text(filter['label'] as String), ], ), ) : ShadButton.outline( onPressed: () => setState(() => _activeFilter = filter['key'] as String), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(filter['icon'] as IconData, size: 16), const SizedBox(width: 6), Text(filter['label'] as String), ], ), ), ); }).toList(), ), ); } /// 필터링된 유지보수 목록 (테이블 형태) Widget _buildFilteredMaintenanceList(MaintenanceController controller) { if (controller.isLoading && controller.upcomingAlerts.isEmpty && controller.overdueAlerts.isEmpty) { return ShadCard( child: SizedBox( height: 200, child: const Center( child: CircularProgressIndicator(), ), ), ); } final filteredList = _getFilteredMaintenanceList(controller); if (filteredList.isEmpty) { return StandardDataTable( columns: _buildTableColumns(), rows: const [], emptyMessage: _getEmptyMessage(), emptyIcon: Icons.check_circle_outline, ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 제목 헤더 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: ShadcnTheme.muted, borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _getFilterTitle(), style: ShadcnTheme.bodyLarge.copyWith( fontWeight: FontWeight.w600, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: ShadcnTheme.primary, borderRadius: BorderRadius.circular(12), ), child: Text( '${filteredList.length}건', style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.primaryForeground, fontWeight: FontWeight.w600, ), ), ), ], ), ), // 테이블 StandardDataTable( columns: _buildTableColumns(), rows: filteredList.map((maintenance) => _buildMaintenanceTableRow(maintenance, controller) ).toList(), maxHeight: 400, ), ], ); } /// 테이블 컬럼 정의 List _buildTableColumns() { return [ StandardDataColumn( label: '장비명', flex: 3, ), StandardDataColumn( label: '시리얼번호', flex: 2, ), StandardDataColumn( label: '고객사', flex: 2, ), StandardDataColumn( label: '만료일', flex: 2, ), StandardDataColumn( label: '타입', flex: 1, ), StandardDataColumn( label: '상태', flex: 2, ), StandardDataColumn( label: '주기', flex: 1, ), ]; } /// 유지보수 테이블 행 생성 StandardDataRow _buildMaintenanceTableRow( MaintenanceDto maintenance, MaintenanceController controller, ) { // 만료까지 남은 일수 계산 final today = DateTime.now(); final daysRemaining = maintenance.endedAt.difference(today).inDays; final isExpiringSoon = daysRemaining <= 7; final isExpired = daysRemaining < 0; return StandardDataRow( index: 0, // index는 StandardDataTable에서 자동 설정 columns: _buildTableColumns(), cells: [ // 장비명 InkWell( onTap: () => _showMaintenanceDetails(maintenance), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Text( controller.getEquipmentName(maintenance), style: ShadcnTheme.bodyMedium.copyWith( fontWeight: FontWeight.w500, color: ShadcnTheme.primary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), // 시리얼번호 Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Text( controller.getEquipmentSerial(maintenance), style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foreground, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // 고객사 - Phase 10: 회사 타입별 색상 적용 Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Text( controller.getCompanyName(maintenance), style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.companyCustomer, // 고객사 - 진한 그린 fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // 만료일 Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Text( '${maintenance.endedAt.year}-${maintenance.endedAt.month.toString().padLeft(2, '0')}-${maintenance.endedAt.day.toString().padLeft(2, '0')}', style: ShadcnTheme.bodySmall.copyWith( // Phase 10: 만료 상태별 색상 체계 적용 color: isExpired ? ShadcnTheme.alertExpired // 만료됨 - 심각 (진한 레드) : isExpiringSoon ? ShadcnTheme.alertWarning30 // 만료 임박 - 경고 (오렌지) : ShadcnTheme.alertNormal, // 정상 - 안전 (그린) fontWeight: isExpired || isExpiringSoon ? FontWeight.w600 : FontWeight.w500, ), ), ), // 타입 (방문/원격 변환) Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _getMaintenanceTypeColor(maintenance.maintenanceType), borderRadius: BorderRadius.circular(12), ), child: Text( _getMaintenanceTypeLabel(maintenance.maintenanceType), style: ShadcnTheme.caption.copyWith( color: Colors.white, fontWeight: FontWeight.w600, fontSize: 10, ), ), ), ), // 상태 (남은 일수/지연) Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Text( isExpired ? '${daysRemaining.abs()}일 지연' : '$daysRemaining일 남음', style: ShadcnTheme.bodySmall.copyWith( // Phase 10: 남은 일수 상태별 색상 체계 적용 color: isExpired ? ShadcnTheme.alertExpired // 지연 - 심각 (진한 레드) : isExpiringSoon ? ShadcnTheme.alertWarning30 // 임박 - 경고 (오렌지) : ShadcnTheme.alertNormal, // 충분 - 안전 (그린) fontWeight: FontWeight.w600, ), ), ), // 주기 Padding( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: Text( '${maintenance.periodMonth}개월', style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.foreground, ), ), ), ], ); } /// 유지보수 타입을 방문(V)/원격(R)로 변환 String _getMaintenanceTypeLabel(String maintenanceType) { switch (maintenanceType) { case 'V': return '방문'; case 'R': return '원격'; default: return maintenanceType; } } /// 유지보수 타입별 색상 // Phase 10: 유지보수 타입별 색상 체계 Color _getMaintenanceTypeColor(String maintenanceType) { switch (maintenanceType) { case 'V': // 방문 - 본사/지점 계열 (블루) return ShadcnTheme.companyHeadquarters; case 'R': // 원격 - 협력/성장 계열 (그린) return ShadcnTheme.companyPartner; default: return ShadcnTheme.muted; } } /// 빠른 작업 섹션 Widget _buildQuickActions() { return ShadCard( child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '빠른 작업', style: ShadcnTheme.headingH4.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), Wrap( spacing: 12, runSpacing: 12, children: [ ShadButton( onPressed: _showCreateMaintenanceDialog, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add_circle, size: 18), SizedBox(width: 8), Text('새 유지보수 등록'), ], ), ), ShadButton.outline( onPressed: () => Navigator.pushNamed(context, '/maintenance/schedule'), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.calendar_month, size: 18), SizedBox(width: 8), Text('일정 보기'), ], ), ), ShadButton.outline( onPressed: _generateReport, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.description, size: 18), SizedBox(width: 8), Text('보고서 생성'), ], ), ), ], ), ], ), ), ); } // === 이벤트 핸들러 === /// 카드 클릭 핸들러 void _handleCardTap(String cardType) { setState(() { _activeFilter = cardType; }); } /// 데이터 새로고침 Future _refreshData() async { final dashboardController = context.read(); final maintenanceController = context.read(); await Future.wait([ dashboardController.refreshDashboardStats(), maintenanceController.loadAlerts(), ]); } /// 필터링된 목록 조회 List _getFilteredMaintenanceList(MaintenanceController controller) { switch (_activeFilter) { case 'expiring_7': return controller.upcomingAlerts.where((m) { final scheduledDate = DateTime(m.startedAt.year, m.startedAt.month + m.periodMonth, m.startedAt.day); final daysUntil = scheduledDate.difference(DateTime.now()).inDays; return daysUntil >= 0 && daysUntil <= 7; }).toList(); case 'expiring_30': return controller.upcomingAlerts.where((m) { final scheduledDate = DateTime(m.startedAt.year, m.startedAt.month + m.periodMonth, m.startedAt.day); final daysUntil = scheduledDate.difference(DateTime.now()).inDays; return daysUntil >= 8 && daysUntil <= 30; }).toList(); case 'expiring_60': return controller.upcomingAlerts.where((m) { final scheduledDate = DateTime(m.startedAt.year, m.startedAt.month + m.periodMonth, m.startedAt.day); final daysUntil = scheduledDate.difference(DateTime.now()).inDays; return daysUntil >= 31 && daysUntil <= 60; }).toList(); case 'expired': return controller.overdueAlerts; case 'all': default: return [...controller.upcomingAlerts, ...controller.overdueAlerts]; } } /// 필터별 제목 String _getFilterTitle() { switch (_activeFilter) { case 'expiring_7': return '7일 내 만료 예정'; case 'expiring_30': return '30일 내 만료 예정'; case 'expiring_60': return '60일 내 만료 예정'; case 'expired': return '만료된 계약'; case 'all': default: return '전체 유지보수 목록'; } } /// 빈 목록 메시지 String _getEmptyMessage() { switch (_activeFilter) { case 'expiring_7': return '7일 내 만료 예정인 유지보수가 없습니다'; case 'expiring_30': return '30일 내 만료 예정인 유지보수가 없습니다'; case 'expiring_60': return '60일 내 만료 예정인 유지보수가 없습니다'; case 'expired': return '만료된 계약이 없습니다'; case 'all': default: return '등록된 유지보수가 없습니다'; } } /// 유지보수 상세 보기 void _showMaintenanceDetails(MaintenanceDto maintenance) { showDialog( context: context, builder: (context) => MaintenanceFormDialog(maintenance: maintenance), ).then((result) { if (result == true) { _refreshData(); } }); } /// 새 유지보수 등록 다이얼로그 void _showCreateMaintenanceDialog() { showDialog( context: context, builder: (context) => const MaintenanceFormDialog(), ).then((result) { if (result == true) { _refreshData(); } }); } /// 보고서 생성 (플레이스홀더) void _generateReport() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('보고서 생성 기능은 준비 중입니다'), backgroundColor: Colors.orange, ), ); } }