import 'dart:math' as math; 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'; // Removed StandardDataTable in favor of ShadTable.list import 'package:superport/screens/common/widgets/pagination.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 int _alertPage = 1; final int _alertPageSize = 10; @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(), ), ); } /// 필터링된 유지보수 목록 (ShadTable.list) 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); // 항상 테이블 헤더 + 페이지네이션을 유지 // 페이지네이션 적용 (UI 레벨) final total = filteredList.length; final start = ((_alertPage - 1) * _alertPageSize).clamp(0, total); final end = (start + _alertPageSize).clamp(0, total); final paged = filteredList.sublist(start, end); 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, ), ), ), ], ), ), // 테이블 (ShadTable.list) // 주의: TwoDimensionalViewport 제약 오류 방지를 위해 고정/제한 높이 부여 SizedBox( height: _computeTableHeight(paged.length), child: LayoutBuilder(builder: (context, constraints) { // 최소 폭 추정: 장비명(260)+시리얼(200)+고객사(240)+만료일(140)+타입(120)+상태(180)+주기(100)+여백(24) const double minTableWidth = 260 + 200 + 240 + 140 + 120 + 180 + 100 + 24; final double extra = constraints.maxWidth - minTableWidth; final double fillerWidth = extra > 0 ? extra : 0.0; const double nameBaseWidth = 260.0; final double nameColumnWidth = nameBaseWidth + fillerWidth; // 남는 폭은 장비명이 흡수 return ShadTable.list( header: [ ShadTableCell.header(child: SizedBox(width: nameColumnWidth, child: const Text('장비명'))), const ShadTableCell.header(child: Text('시리얼번호')), const ShadTableCell.header(child: Text('고객사')), const ShadTableCell.header(child: Text('만료일')), const ShadTableCell.header(child: Text('타입')), const ShadTableCell.header(child: Text('상태')), const ShadTableCell.header(child: Text('주기')), ], // 남은 폭을 채우는 필러 컬럼은 위에서 header에 추가함 children: paged.map((maintenance) { final today = DateTime.now(); final daysRemaining = maintenance.endedAt.difference(today).inDays; final isExpiringSoon = daysRemaining <= 7; final isExpired = daysRemaining < 0; Color typeBg = _getMaintenanceTypeColor(maintenance.maintenanceType); final typeLabel = _getMaintenanceTypeLabel(maintenance.maintenanceType); return [ // 장비명 ShadTableCell( child: SizedBox( width: nameColumnWidth, child: InkWell( onTap: () => _showMaintenanceDetails(maintenance), child: Text( controller.getEquipmentName(maintenance), style: ShadcnTheme.bodyMedium.copyWith( fontWeight: FontWeight.w500, color: ShadcnTheme.primary, ), overflow: TextOverflow.ellipsis, ), ), ), ), // 시리얼번호 ShadTableCell( child: Text( controller.getEquipmentSerial(maintenance), style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ), // 고객사 ShadTableCell( child: Text( controller.getCompanyName(maintenance), style: ShadcnTheme.bodySmall.copyWith( color: ShadcnTheme.companyCustomer, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), // 만료일 ShadTableCell( child: Text( '${maintenance.endedAt.year}-${maintenance.endedAt.month.toString().padLeft(2, '0')}-${maintenance.endedAt.day.toString().padLeft(2, '0')}', style: ShadcnTheme.bodySmall.copyWith( color: isExpired ? ShadcnTheme.alertExpired : isExpiringSoon ? ShadcnTheme.alertWarning30 : ShadcnTheme.alertNormal, fontWeight: isExpired || isExpiringSoon ? FontWeight.w600 : FontWeight.w500, ), ), ), // 타입 ShadTableCell( child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: typeBg, borderRadius: BorderRadius.circular(12), ), child: Text( typeLabel, style: ShadcnTheme.caption.copyWith( color: ShadcnTheme.primaryForeground, fontWeight: FontWeight.w600, fontSize: 10, ), ), ), ), // 상태 (남은 일수/지연) ShadTableCell( child: Text( isExpired ? '${daysRemaining.abs()}일 지연' : '$daysRemaining일 남음', style: ShadcnTheme.bodySmall.copyWith( color: isExpired ? ShadcnTheme.alertExpired : isExpiringSoon ? ShadcnTheme.alertWarning30 : ShadcnTheme.alertNormal, fontWeight: FontWeight.w600, ), ), ), // 주기 ShadTableCell( child: Text( '${maintenance.periodMonth}개월', style: ShadcnTheme.bodySmall, ), ), // 바디에는 빈 셀로 컬럼만 유지 // 남는 폭은 장비명 컬럼이 흡수 ]; }).toList(), ); }), ), // 하단 페이지네이션 (항상 표시) const SizedBox(height: 12), Pagination( totalCount: total, currentPage: _alertPage, pageSize: _alertPageSize, onPageChanged: (p) => setState(() => _alertPage = p), ), ], ); } /// 유지보수 타입을 방문(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; } } // 테이블 높이 계산 (ShadTable.list가 내부 스크롤을 가지므로 부모에서 높이를 제한) double _computeTableHeight(int rows) { const rowHeight = 48.0; // 셀 높이(대략) const headerHeight = 48.0; const minH = 200.0; // 너무 작지 않게 최소 높이 const maxH = 560.0; // 페이지에서 과도하게 커지지 않도록 상한 final visible = rows.clamp(1, _alertPageSize); // 페이지당 행 수 이내로 계산 final h = headerHeight + (visible * rowHeight) + 16.0; // 약간의 패딩 return math.max(minH, math.min(maxH, h.toDouble())); } /// 빠른 작업 섹션 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, ), ); } }