import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:get_it/get_it.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/widgets/pagination.dart'; import 'package:superport/screens/common/widgets/standard_action_bar.dart'; import 'package:superport/screens/common/widgets/standard_states.dart'; import 'package:superport/screens/maintenance/controllers/maintenance_controller.dart'; import 'package:superport/screens/maintenance/maintenance_form_dialog.dart'; import 'package:superport/data/models/maintenance_dto.dart'; import 'package:superport/data/repositories/equipment_history_repository.dart'; import 'package:superport/domain/usecases/maintenance_usecase.dart'; /// shadcn/ui 스타일로 설계된 유지보수 관리 화면 class MaintenanceList extends StatefulWidget { const MaintenanceList({super.key}); @override State createState() => _MaintenanceListState(); } class _MaintenanceListState extends State { late final MaintenanceController _controller; bool _showDetailedColumns = true; final TextEditingController _searchController = TextEditingController(); final ScrollController _horizontalScrollController = ScrollController(); final Set _selectedItems = {}; @override void initState() { super.initState(); _controller = MaintenanceController( maintenanceUseCase: GetIt.instance(), equipmentHistoryRepository: GetIt.instance(), ); // 초기 데이터 로드 WidgetsBinding.instance.addPostFrameCallback((_) { _controller.loadMaintenances(refresh: true); _controller.loadExpiringMaintenances(); }); } @override void dispose() { _searchController.dispose(); _horizontalScrollController.dispose(); _controller.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); _adjustColumnsForScreenSize(); } /// 화면 크기에 따라 컬럼 표시 조정 void _adjustColumnsForScreenSize() { final width = MediaQuery.of(context).size.width; setState(() { _showDetailedColumns = width > 1000; }); } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, child: Scaffold( backgroundColor: ShadcnTheme.background, body: Consumer( builder: (context, controller, child) { return Column( children: [ _buildActionBar(), _buildFilterBar(), Expanded(child: _buildMainContent()), Container( padding: const EdgeInsets.all(ShadcnTheme.spacing4), decoration: BoxDecoration( color: ShadcnTheme.card, border: Border( top: BorderSide(color: ShadcnTheme.border), ), ), child: _buildPagination(controller), ), ], ); }, ), ), ); } /// 상단 액션바 Widget _buildActionBar() { return Consumer( builder: (context, controller, child) { return StandardActionBar( totalCount: controller.totalCount, selectedCount: _selectedItems.length, leftActions: const [ Text('유지보수 관리', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ], rightActions: [ // 만료 예정 알림 if (controller.expiringMaintenances.isNotEmpty) ShadButton.outline( child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.notification_important, size: 16), const SizedBox(width: 4), Text('만료 예정 ${controller.expiringMaintenances.length}건'), ], ), onPressed: () => _showExpiringMaintenances(), ), // 새로운 유지보수 등록 ShadButton( child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add, size: 16), SizedBox(width: 4), Text('유지보수 등록'), ], ), onPressed: () => _showMaintenanceForm(), ), // 선택된 항목 삭제 if (_selectedItems.isNotEmpty) ShadButton.destructive( child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.delete, size: 16), const SizedBox(width: 4), Text('삭제 (${_selectedItems.length})'), ], ), onPressed: () => _showDeleteConfirmation(), ), ], ); }, ); } /// 필터바 Widget _buildFilterBar() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: ShadcnTheme.card, border: Border( bottom: BorderSide(color: ShadcnTheme.border), ), ), child: Row( children: [ // 유지보수 타입 필터 Expanded( flex: 2, child: ShadSelect( placeholder: const Text('유지보수 타입'), options: [ const ShadOption(value: 'all', child: Text('전체')), ...MaintenanceType.typeOptions.map((option) => ShadOption( value: option['value']!, child: Text(option['label']!), ), ), ], selectedOptionBuilder: (context, value) { if (value == 'all') return const Text('전체'); final option = MaintenanceType.typeOptions .firstWhere((o) => o['value'] == value, orElse: () => {'label': value}); return Text(option['label']!); }, onChanged: (value) { _controller.setMaintenanceTypeFilter( value == 'all' ? null : value, ); }, ), ), const SizedBox(width: 12), // 상태 필터 Expanded( flex: 2, child: ShadSelect( placeholder: const Text('상태'), options: const [ ShadOption(value: 'all', child: Text('전체')), ShadOption(value: 'active', child: Text('활성')), ShadOption(value: 'expired', child: Text('만료')), ShadOption(value: 'expiring', child: Text('만료 예정')), ], selectedOptionBuilder: (context, value) { switch (value) { case 'active': return const Text('활성'); case 'expired': return const Text('만료'); case 'expiring': return const Text('만료 예정'); default: return const Text('전체'); } }, onChanged: (value) { _applyStatusFilter(value ?? 'all'); }, ), ), const SizedBox(width: 12), // 검색 Expanded( flex: 3, child: ShadInput( controller: _searchController, placeholder: const Text('장비 시리얼 번호 또는 모델명 검색...'), onSubmitted: (_) => _performSearch(), ), ), const SizedBox(width: 12), // 필터 초기화 ShadButton.outline( child: const Icon(Icons.refresh, size: 16), onPressed: _resetFilters, ), ], ), ); } /// 메인 컨텐츠 Widget _buildMainContent() { return Consumer( builder: (context, controller, child) { if (controller.isLoading && controller.maintenances.isEmpty) { return const StandardLoadingState(message: '유지보수 목록을 불러오는 중...'); } if (controller.error != null) { return StandardErrorState( message: controller.error!, onRetry: () => controller.loadMaintenances(refresh: true), ); } if (controller.maintenances.isEmpty) { return const StandardEmptyState( icon: Icons.build_circle_outlined, title: '유지보수가 없습니다', message: '새로운 유지보수를 등록해보세요.', ); } return _buildDataTable(controller); }, ); } /// 데이터 테이블 Widget _buildDataTable(MaintenanceController controller) { final maintenances = controller.maintenances; if (maintenances.isEmpty) { return const StandardEmptyState( icon: Icons.build_circle_outlined, title: '유지보수가 없습니다', message: '새로운 유지보수를 등록해보세요.', ); } return LayoutBuilder( builder: (context, constraints) { // 기본 컬럼 폭 합산 const double selectW = 60; const double idW = 80; const double equipInfoBaseW = 200; // 이 컬럼이 남는 폭을 흡수 const double typeW = 120; const double startW = 100; const double endW = 100; const double periodW = 80; const double statusW = 100; const double remainW = 100; const double actionsW = 120; double baseWidth = selectW + idW + equipInfoBaseW + typeW + startW + endW + actionsW; if (_showDetailedColumns) { baseWidth += periodW + statusW + remainW; } final extra = (constraints.maxWidth - baseWidth); final double equipInfoWidth = equipInfoBaseW + (extra > 0 ? extra : 0.0); final double tableWidth = (constraints.maxWidth > baseWidth) ? constraints.maxWidth : baseWidth; return Container( decoration: BoxDecoration( border: Border.all(color: ShadcnTheme.border), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: Column( children: [ // 고정 헤더 Container( padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3), decoration: BoxDecoration( color: ShadcnTheme.muted.withValues(alpha: 0.3), border: Border(bottom: BorderSide(color: ShadcnTheme.border)), borderRadius: BorderRadius.only( topLeft: Radius.circular(ShadcnTheme.radiusMd), topRight: Radius.circular(ShadcnTheme.radiusMd), ), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: _horizontalScrollController, child: _buildFixedHeader(equipInfoWidth, tableWidth), ), ), // 스크롤 가능한 바디 Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: _horizontalScrollController, child: SizedBox( width: tableWidth, child: ListView.builder( itemCount: maintenances.length, itemBuilder: (context, index) => _buildTableRow(maintenances[index], index, equipInfoWidth), ), ), ), ), ], ), ); }, ); } /// 고정 헤더 빌드 Widget _buildFixedHeader(double equipInfoWidth, double tableWidth) { return SizedBox( width: tableWidth, child: Row( children: [ _buildHeaderCell('선택', 60), _buildHeaderCell('ID', 80), _buildHeaderCell('장비 정보', equipInfoWidth), _buildHeaderCell('유지보수 타입', 120), _buildHeaderCell('시작일', 100), _buildHeaderCell('종료일', 100), if (_showDetailedColumns) ...[ _buildHeaderCell('주기', 80), _buildHeaderCell('상태', 100), _buildHeaderCell('남은 일수', 100), ], _buildHeaderCell('작업', 120), ], ), ); } // 기존 _calculateTableWidth 제거: LayoutBuilder에서 계산 /// 헤더 셀 빌드 Widget _buildHeaderCell(String text, double width) { return Container( width: width, padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing2), child: Text( text, style: ShadcnTheme.bodyMedium.copyWith(fontWeight: FontWeight.w500), ), ); } /// 테이블 행 빌드 Widget _buildTableRow(MaintenanceDto maintenance, int index, double equipInfoWidth) { return Container( decoration: BoxDecoration( color: index.isEven ? ShadcnTheme.muted.withValues(alpha: 0.1) : null, border: Border(bottom: BorderSide(color: ShadcnTheme.border.withValues(alpha: 0.3))), ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _showMaintenanceDetail(maintenance), child: Container( padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing4, vertical: ShadcnTheme.spacing3), child: Row( children: [ // 선택 체크박스 SizedBox( width: 60, child: ShadCheckbox( value: _selectedItems.contains(maintenance.id), onChanged: (value) { setState(() { if (value == true) { _selectedItems.add(maintenance.id!); } else { _selectedItems.remove(maintenance.id!); } }); }, ), ), // ID SizedBox( width: 80, child: Text( maintenance.id?.toString() ?? '-', style: ShadcnTheme.bodySmall, ), ), // 장비 정보 (가변 폭) SizedBox( width: equipInfoWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( maintenance.equipmentSerial ?? '시리얼 번호 없음', style: const TextStyle(fontWeight: FontWeight.w500), overflow: TextOverflow.ellipsis, ), if (maintenance.equipmentModel != null) Text( maintenance.equipmentModel!, style: TextStyle( fontSize: 12, color: ShadcnTheme.mutedForeground, ), overflow: TextOverflow.ellipsis, ), ], ), ), // 유지보수 타입 SizedBox( width: 120, child: Container( padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing2, vertical: ShadcnTheme.spacing1), decoration: BoxDecoration( color: _getMaintenanceTypeColor(maintenance.maintenanceType), borderRadius: BorderRadius.circular(ShadcnTheme.radiusSm), ), child: Text( MaintenanceType.getDisplayName(maintenance.maintenanceType), style: ShadcnTheme.caption.copyWith( fontSize: 12, color: ShadcnTheme.primaryForeground, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), ), // 시작일 SizedBox( width: 100, child: Text( DateFormat('yyyy-MM-dd').format(maintenance.startedAt), style: ShadcnTheme.bodySmall, ), ), // 종료일 SizedBox( width: 100, child: Text( DateFormat('yyyy-MM-dd').format(maintenance.endedAt), style: ShadcnTheme.bodySmall, ), ), // 상세 컬럼들 if (_showDetailedColumns) ...[ // 주기 SizedBox( width: 80, child: Text( '${maintenance.periodMonth}개월', style: ShadcnTheme.bodySmall, ), ), // 상태 SizedBox( width: 100, child: Container( padding: const EdgeInsets.symmetric(horizontal: ShadcnTheme.spacing2, vertical: ShadcnTheme.spacing1), decoration: BoxDecoration( color: _controller.getMaintenanceStatusColor(maintenance), borderRadius: BorderRadius.circular(ShadcnTheme.radiusSm), ), child: Text( _controller.getMaintenanceStatusText(maintenance), style: ShadcnTheme.caption.copyWith( fontSize: 12, color: ShadcnTheme.primaryForeground, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, ), ), ), // 남은 일수 SizedBox( width: 100, child: Text( maintenance.daysRemaining != null ? '${maintenance.daysRemaining}일' : '-', style: TextStyle( color: maintenance.daysRemaining != null && maintenance.daysRemaining! <= 30 ? ShadcnTheme.destructive : ShadcnTheme.foreground, ), ), ), ], // 작업 버튼들 SizedBox( width: 120, child: Row( mainAxisSize: MainAxisSize.min, children: [ ShadButton.ghost( child: const Icon(Icons.edit, size: 16), onPressed: () => _showMaintenanceForm(maintenance: maintenance), ), const SizedBox(width: ShadcnTheme.spacing1), ShadButton.ghost( child: Icon( Icons.delete, size: 16, color: ShadcnTheme.destructive, ), onPressed: () => _deleteMaintenance(maintenance), ), ], ), ), ], ), ), ), ), ); } /// 하단 페이지네이션 Widget _buildPagination(MaintenanceController controller) { return Pagination( totalCount: controller.totalCount, currentPage: controller.currentPage, pageSize: 20, // MaintenanceController._perPage 상수값 onPageChanged: (page) => controller.goToPage(page), ); } // 유틸리티 메서드들 Color _getMaintenanceTypeColor(String type) { switch (type) { case MaintenanceType.visit: return Colors.blue; case MaintenanceType.remote: return Colors.green; default: return Colors.grey; } } void _applyStatusFilter(String status) { switch (status) { case 'active': _controller.setExpiredFilter(false); break; case 'expired': _controller.setExpiredFilter(true); break; case 'expiring': _controller.setExpiringDaysFilter(30); break; default: _controller.setExpiredFilter(null); _controller.setExpiringDaysFilter(null); } } void _performSearch() { // TODO: 장비 시리얼/모델 검색 구현 // 백엔드에 검색 API가 추가되면 구현 } void _resetFilters() { setState(() { _searchController.clear(); }); _controller.clearFilters(); } // 다이얼로그 메서드들 void _showMaintenanceForm({MaintenanceDto? maintenance}) { showDialog( context: context, builder: (context) => ChangeNotifierProvider.value( value: _controller, child: MaintenanceFormDialog(maintenance: maintenance), ), ).then((_) { // 폼 닫힌 후 목록 새로고침 _controller.loadMaintenances(refresh: true); }); } void _showMaintenanceDetail(MaintenanceDto maintenance) { _controller.selectMaintenance(maintenance); _showMaintenanceForm(maintenance: maintenance); } void _showExpiringMaintenances() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('만료 예정 유지보수'), content: SizedBox( width: 600, height: 400, child: ListView.builder( itemCount: _controller.expiringMaintenances.length, itemBuilder: (context, index) { final maintenance = _controller.expiringMaintenances[index]; return ListTile( title: Text(maintenance.equipmentSerial ?? '시리얼 번호 없음'), subtitle: Text( '${MaintenanceType.getDisplayName(maintenance.maintenanceType)} - ' '${DateFormat('yyyy-MM-dd').format(maintenance.endedAt)} 만료', ), trailing: maintenance.daysRemaining != null ? Text( '${maintenance.daysRemaining}일 남음', style: TextStyle( color: maintenance.daysRemaining! <= 7 ? Colors.red : Colors.orange, fontWeight: FontWeight.bold, ), ) : null, onTap: () { Navigator.pop(context); _showMaintenanceDetail(maintenance); }, ); }, ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('닫기'), ), ], ), ); } void _deleteMaintenance(MaintenanceDto maintenance) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('유지보수 삭제'), content: Text( '${maintenance.equipmentSerial ?? "선택된"} 장비의 유지보수를 삭제하시겠습니까?\n' '삭제된 데이터는 복구할 수 있습니다.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('취소'), ), TextButton( onPressed: () async { Navigator.pop(context); final success = await _controller.deleteMaintenance(maintenance.id!); if (success && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('유지보수가 삭제되었습니다')), ); } }, child: const Text('삭제', style: TextStyle(color: Colors.red)), ), ], ), ); } void _showDeleteConfirmation() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('선택된 유지보수 삭제'), content: Text('선택된 ${_selectedItems.length}개의 유지보수를 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('취소'), ), TextButton( onPressed: () async { Navigator.pop(context); // TODO: 일괄 삭제 구현 setState(() { _selectedItems.clear(); }); }, child: const Text('삭제', style: TextStyle(color: Colors.red)), ), ], ), ); } }