import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.dart'; import 'package:superport/screens/overview/controllers/overview_controller.dart'; // MockDataService 제거 - 실제 API 사용 import 'package:superport/services/auth_service.dart'; import 'package:superport/services/health_check_service.dart'; import 'package:superport/core/widgets/auth_guard.dart'; import 'package:superport/data/models/auth/auth_user.dart'; import 'package:superport/screens/overview/widgets/license_expiry_alert.dart'; import 'package:superport/screens/overview/widgets/statistics_card_grid.dart'; /// shadcn/ui 스타일로 재설계된 대시보드 화면 class OverviewScreen extends StatefulWidget { const OverviewScreen({super.key}); @override State createState() => _OverviewScreenState(); } class _OverviewScreenState extends State { late final OverviewController _controller; late final HealthCheckService _healthCheckService; Map? _healthStatus; bool _isHealthCheckLoading = false; @override void initState() { super.initState(); _controller = OverviewController(); _healthCheckService = HealthCheckService(); _loadData(); _checkHealthStatus(); // 주기적인 헬스체크 시작 (30초마다) _healthCheckService.startPeriodicHealthCheck(); } Future _loadData() async { await _controller.loadDashboardData(); } Future _checkHealthStatus() async { setState(() { _isHealthCheckLoading = true; }); final result = await _healthCheckService.checkHealth(); if (mounted) { setState(() { _healthStatus = result; _isHealthCheckLoading = false; }); } } @override void dispose() { _controller.dispose(); _healthCheckService.stopPeriodicHealthCheck(); super.dispose(); } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, child: Consumer( builder: (context, controller, child) { if (controller.isLoading) { return _buildLoadingState(); } if (controller.error != null) { return _buildErrorState(controller.error!); } return Container( color: ShadcnTheme.background, child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 라이선스 만료 알림 배너 (조건부 표시) if (controller.licenseExpirySummary != null) ...[ LicenseExpiryAlert(summary: controller.licenseExpirySummary!), const SizedBox(height: 16), ], // 환영 섹션 ShadcnCard( padding: const EdgeInsets.all(32), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FutureBuilder( future: context.read().getCurrentUser(), builder: (context, snapshot) { final userName = snapshot.data?.name ?? '사용자'; return Text('안녕하세요, $userName님! 👋', style: ShadcnTheme.headingH3); }, ), const SizedBox(height: 8), Text( '오늘의 포트 운영 현황을 확인해보세요.', style: ShadcnTheme.bodyMuted, ), const SizedBox(height: 16), Row( children: [ ShadcnBadge( text: '실시간 모니터링', variant: ShadcnBadgeVariant.success, ), const SizedBox(width: 8), ShadcnBadge( text: '업데이트됨', variant: ShadcnBadgeVariant.outline, ), ], ), ], ), ), ], ), ), const SizedBox(height: 32), // 통계 카드 그리드 (새로운 위젯) if (controller.overviewStats != null) StatisticsCardGrid(stats: controller.overviewStats!), const SizedBox(height: 32), // 하단 콘텐츠 LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth > 1000) { // 큰 화면: 가로로 배치 return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 2, child: _buildLeftColumn()), const SizedBox(width: 24), Expanded(flex: 1, child: _buildRightColumn()), ], ); } else { // 작은 화면: 세로로 배치 return Column( children: [ _buildLeftColumn(), const SizedBox(height: 24), _buildRightColumn(), ], ); } }, ), ], ), ), ); }, ), ); } Widget _buildLoadingState() { return Container( color: ShadcnTheme.background, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: ShadcnTheme.primary), const SizedBox(height: ShadcnTheme.spacing4), Text('대시보드를 불러오는 중...', style: ShadcnTheme.bodyMuted), ], ), ), ); } Widget _buildErrorState(String error) { return Container( color: ShadcnTheme.background, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 48, color: ShadcnTheme.error, ), const SizedBox(height: ShadcnTheme.spacing4), Text('오류가 발생했습니다', style: ShadcnTheme.headingH4), const SizedBox(height: ShadcnTheme.spacing2), Text(error, style: ShadcnTheme.bodyMuted), const SizedBox(height: ShadcnTheme.spacing4), ShadcnButton( text: '다시 시도', onPressed: _loadData, variant: ShadcnButtonVariant.primary, ), ], ), ), ); } Widget _buildLeftColumn() { return Column( children: [ // 차트 카드 ShadcnCard( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('월별 활동 현황', style: ShadcnTheme.headingH4), Text('최근 6개월 데이터', style: ShadcnTheme.bodyMuted), ], ), ShadcnButton( text: '상세보기', onPressed: () {}, variant: ShadcnButtonVariant.ghost, size: ShadcnButtonSize.small, ), ], ), const SizedBox(height: 24), Container( height: 200, decoration: BoxDecoration( color: ShadcnTheme.muted, borderRadius: BorderRadius.circular(8), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.analytics, size: 48, color: ShadcnTheme.mutedForeground, ), const SizedBox(height: 12), Text('차트 영역', style: ShadcnTheme.bodyMuted), Text( 'fl_chart 라이브러리로 구현 예정', style: ShadcnTheme.bodySmall, ), ], ), ), ), ], ), ), const SizedBox(height: 24), // 최근 활동 ShadcnCard( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('최근 활동', style: ShadcnTheme.headingH4), ShadcnButton( text: '전체보기', onPressed: () {}, variant: ShadcnButtonVariant.ghost, size: ShadcnButtonSize.small, ), ], ), const SizedBox(height: 16), Consumer( builder: (context, controller, child) { final activities = controller.recentActivities; if (activities.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Center( child: Text( '최근 활동이 없습니다', style: ShadcnTheme.bodyMuted, ), ), ); } return Column( children: activities.take(5).map((activity) => _buildActivityItem(activity), ).toList(), ); }, ), ], ), ), ], ); } Widget _buildRightColumn() { return FutureBuilder( future: context.read().getCurrentUser(), builder: (context, snapshot) { final userRole = snapshot.data?.role?.toLowerCase() ?? ''; final isAdminOrManager = userRole == 'admin' || userRole == 'manager'; return Column( children: [ // 빠른 작업 (Admin과 Manager만 표시) if (isAdminOrManager) ...[ ShadcnCard( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('빠른 작업', style: ShadcnTheme.headingH4), const SizedBox(height: 16), _buildQuickActionButton(Icons.add_box, '장비 입고', '새 장비 등록'), const SizedBox(height: 12), _buildQuickActionButton( Icons.local_shipping, '장비 출고', '장비 대여 처리', ), const SizedBox(height: 12), _buildQuickActionButton( Icons.business_center, '회사 등록', '새 회사 추가', ), ], ), ), const SizedBox(height: 24), ], // 시스템 상태 (실시간 헬스체크) ShadcnCard( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('시스템 상태', style: ShadcnTheme.headingH4), IconButton( icon: _isHealthCheckLoading ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(ShadcnTheme.primary), ), ) : Icon(Icons.refresh, size: 20, color: ShadcnTheme.muted), onPressed: _isHealthCheckLoading ? null : _checkHealthStatus, tooltip: '새로고침', ), ], ), const SizedBox(height: 16), _buildHealthStatusItem('서버 상태', _getServerStatus()), _buildHealthStatusItem('데이터베이스', _getDatabaseStatus()), _buildHealthStatusItem('API 응답', _getApiResponseTime()), _buildHealthStatusItem('최종 체크', _getLastCheckTime()), ], ), ), ], ); }, ); } Widget _buildActivityItem(dynamic activity) { // 아이콘 매핑 IconData getActivityIcon(String? type) { switch (type?.toLowerCase()) { case 'equipment_in': case '장비 입고': return Icons.inventory; case 'equipment_out': case '장비 출고': return Icons.local_shipping; case 'company': case '회사': return Icons.business; case 'user': case '사용자': return Icons.person_add; default: return Icons.settings; } } // 색상 매핑 Color getActivityColor(String? type) { switch (type?.toLowerCase()) { case 'equipment_in': case '장비 입고': return ShadcnTheme.success; case 'equipment_out': case '장비 출고': return ShadcnTheme.warning; case 'company': case '회사': return ShadcnTheme.info; case 'user': case '사용자': return ShadcnTheme.primary; default: return ShadcnTheme.mutedForeground; } } final activityType = activity.activityType ?? ''; final color = getActivityColor(activityType); final dateFormat = DateFormat('MM/dd HH:mm'); final timestamp = activity.timestamp ?? DateTime.now(); final entityName = activity.entityName ?? '이름 없음'; final description = activity.description ?? '설명 없음'; return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), ), child: Icon( getActivityIcon(activityType), color: color, size: 16, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( entityName, style: ShadcnTheme.bodyMedium, overflow: TextOverflow.ellipsis, ), Text( description, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ], ), ), Text( dateFormat.format(timestamp), style: ShadcnTheme.bodySmall, ), ], ), ); } Widget _buildQuickActionButton(IconData icon, String title, String subtitle) { return GestureDetector( onTap: () { // 실제 기능 구현 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('$title 기능은 개발 중입니다.'), backgroundColor: ShadcnTheme.info, ), ); }, child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(6), ), child: Row( children: [ Icon(icon, color: ShadcnTheme.primary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: ShadcnTheme.bodyMedium), Text(subtitle, style: ShadcnTheme.bodySmall), ], ), ), Icon( Icons.arrow_forward_ios, size: 16, color: ShadcnTheme.mutedForeground, ), ], ), ), ); } Widget _buildStatusItem(String label, String status) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: ShadcnTheme.bodyMedium), ShadcnBadge( text: status, variant: ShadcnBadgeVariant.success, size: ShadcnBadgeSize.small, ), ], ), ); } /// 헬스 상태 아이템 빌더 Widget _buildHealthStatusItem(String label, Map statusInfo) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: ShadcnTheme.bodyMedium), Row( children: [ if (statusInfo['icon'] != null) ...[ Icon( statusInfo['icon'] as IconData, size: 16, color: statusInfo['color'] as Color, ), const SizedBox(width: 4), ], ShadcnBadge( text: statusInfo['text'] as String, variant: statusInfo['variant'] as ShadcnBadgeVariant, size: ShadcnBadgeSize.small, ), ], ), ], ), ); } /// 서버 상태 가져오기 Map _getServerStatus() { if (_healthStatus == null) { return { 'text': '확인 중', 'variant': ShadcnBadgeVariant.secondary, 'icon': Icons.pending, 'color': ShadcnTheme.muted, }; } final isHealthy = _healthStatus!['success'] == true && _healthStatus!['data']?['status'] == 'healthy'; return { 'text': isHealthy ? '정상' : '오류', 'variant': isHealthy ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.destructive, 'icon': isHealthy ? Icons.check_circle : Icons.error, 'color': isHealthy ? ShadcnTheme.success : ShadcnTheme.destructive, }; } /// 데이터베이스 상태 가져오기 Map _getDatabaseStatus() { if (_healthStatus == null) { return { 'text': '확인 중', 'variant': ShadcnBadgeVariant.secondary, 'icon': Icons.pending, 'color': ShadcnTheme.muted, }; } final dbStatus = _healthStatus!['data']?['database']?['status'] ?? 'unknown'; final isConnected = dbStatus == 'connected'; return { 'text': isConnected ? '연결됨' : '연결 안됨', 'variant': isConnected ? ShadcnBadgeVariant.success : ShadcnBadgeVariant.warning, 'icon': isConnected ? Icons.storage : Icons.cloud_off, 'color': isConnected ? ShadcnTheme.success : ShadcnTheme.warning, }; } /// API 응답 시간 가져오기 Map _getApiResponseTime() { if (_healthStatus == null) { return { 'text': '측정 중', 'variant': ShadcnBadgeVariant.secondary, 'icon': Icons.timer, 'color': ShadcnTheme.muted, }; } final responseTime = _healthStatus!['data']?['responseTime'] ?? 0; final timeMs = responseTime is num ? responseTime : 0; ShadcnBadgeVariant variant; Color color; if (timeMs < 100) { variant = ShadcnBadgeVariant.success; color = ShadcnTheme.success; } else if (timeMs < 500) { variant = ShadcnBadgeVariant.warning; color = ShadcnTheme.warning; } else { variant = ShadcnBadgeVariant.destructive; color = ShadcnTheme.destructive; } return { 'text': '${timeMs}ms', 'variant': variant, 'icon': Icons.speed, 'color': color, }; } /// 마지막 체크 시간 가져오기 Map _getLastCheckTime() { if (_healthStatus == null) { return { 'text': '없음', 'variant': ShadcnBadgeVariant.secondary, 'icon': Icons.access_time, 'color': ShadcnTheme.muted, }; } final timestamp = _healthStatus!['data']?['timestamp']; if (timestamp != null) { try { final date = DateTime.parse(timestamp); final formatter = DateFormat('HH:mm:ss'); return { 'text': formatter.format(date), 'variant': ShadcnBadgeVariant.outline, 'icon': Icons.access_time, 'color': ShadcnTheme.foreground, }; } catch (e) { // 파싱 실패 } } // 현재 시간 사용 final now = DateTime.now(); final formatter = DateFormat('HH:mm:ss'); return { 'text': formatter.format(now), 'variant': ShadcnBadgeVariant.outline, 'icon': Icons.access_time, 'color': ShadcnTheme.foreground, }; } }