import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; import 'package:superport/data/models/equipment/equipment_history_dto.dart'; import 'package:superport/services/equipment_service.dart'; import 'package:superport/core/errors/failures.dart'; import 'package:intl/intl.dart'; /// 장비 이력을 표시하는 팝업 다이얼로그 class EquipmentHistoryDialog extends StatefulWidget { final int equipmentId; final String equipmentName; const EquipmentHistoryDialog({ Key? key, required this.equipmentId, required this.equipmentName, }) : super(key: key); @override State createState() => _EquipmentHistoryDialogState(); /// 다이얼로그를 표시하는 정적 메서드 static Future show({ required BuildContext context, required int equipmentId, required String equipmentName, }) { return showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return EquipmentHistoryDialog( equipmentId: equipmentId, equipmentName: equipmentName, ); }, ); } } class _EquipmentHistoryDialogState extends State { final EquipmentService _equipmentService = GetIt.instance(); final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); List _histories = []; List _filteredHistories = []; bool _isLoading = false; bool _isInitialLoad = true; String? _error; int _currentPage = 1; final int _perPage = 20; bool _hasMore = true; String _searchQuery = ''; @override void initState() { super.initState(); _loadHistory(isInitialLoad: true); _scrollController.addListener(_onScroll); _searchController.addListener(_onSearchChanged); } @override void dispose() { _scrollController.dispose(); _searchController.dispose(); super.dispose(); } void _onScroll() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { _loadMoreHistory(); } } void _onSearchChanged() { setState(() { _searchQuery = _searchController.text.toLowerCase(); _filterHistories(); }); } void _filterHistories() { if (_searchQuery.isEmpty) { _filteredHistories = List.from(_histories); } else { _filteredHistories = _histories.where((history) { final remarks = (history.remarks ?? '').toLowerCase(); final userName = (history.userName ?? '').toLowerCase(); final type = _getTransactionTypeText(history.transactionType).toLowerCase(); return remarks.contains(_searchQuery) || userName.contains(_searchQuery) || type.contains(_searchQuery); }).toList(); } } Future _loadHistory({ bool isRefresh = false, bool isInitialLoad = false }) async { if (isRefresh) { _currentPage = 1; _hasMore = true; _histories.clear(); _filteredHistories.clear(); } if (!isInitialLoad && (!_hasMore || (!isRefresh && _isLoading))) { return; } if (!mounted) return; setState(() { _isLoading = true; _error = null; if (isInitialLoad) _isInitialLoad = false; }); try { final histories = await _equipmentService.getEquipmentHistory( widget.equipmentId, page: _currentPage, perPage: _perPage, ).timeout( const Duration(seconds: 10), onTimeout: () { throw Exception('API 호출 시간 초과 (10초)'); }, ); if (!mounted) return; setState(() { if (isRefresh) { _histories = histories; } else { _histories.addAll(histories); } _filterHistories(); _hasMore = histories.length == _perPage; if (_hasMore) _currentPage++; _isLoading = false; }); } on Failure catch (e) { if (!mounted) return; setState(() { _error = e.message; _isLoading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = '이력을 불러오는 중 오류가 발생했습니다: $e'; _isLoading = false; }); } } Future _loadMoreHistory() async { await _loadHistory(); } String _formatDate(DateTime? date) { if (date == null) return '-'; return DateFormat('yyyy-MM-dd HH:mm').format(date); } String _getTransactionTypeText(String? type) { switch (type) { case 'I': return '입고'; case 'O': return '출고'; case 'R': return '대여'; case 'T': return '반납'; case 'D': return '폐기'; default: return type ?? '-'; } } Color _getTransactionTypeColor(String? type) { switch (type) { case 'I': return Colors.green; case 'O': return Colors.blue; case 'R': return Colors.orange; case 'T': return Colors.teal; case 'D': return Colors.red; default: return Colors.grey; } } Widget _buildHistoryItem(EquipmentHistoryDto history) { final typeColor = _getTransactionTypeColor(history.transactionType); final typeText = _getTransactionTypeText(history.transactionType); return Container( margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade200), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.02), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () {}, // 클릭 효과를 위한 빈 핸들러 child: Padding( padding: const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 타입 아이콘 Container( width: 40, height: 40, decoration: BoxDecoration( color: typeColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( typeText, style: TextStyle( color: typeColor, fontWeight: FontWeight.bold, fontSize: 11, ), ), ), ), const SizedBox(width: 12), // 내용 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( history.remarks ?? '비고 없음', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), overflow: TextOverflow.ellipsis, maxLines: 2, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(4), ), child: Text( '수량: ${history.quantity}', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: 4), Row( children: [ Icon( Icons.access_time, size: 14, color: Colors.grey.shade600, ), const SizedBox(width: 4), Text( _formatDate(history.transactionDate), style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), if (history.userName != null) ...[ const SizedBox(width: 12), Icon( Icons.person_outline, size: 14, color: Colors.grey.shade600, ), const SizedBox(width: 4), Text( history.userName!, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ], ), ], ), ), ], ), ), ), ), ); } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final bool isDesktop = screenSize.width > 768; // 반응형 크기 설정 final dialogWidth = isDesktop ? screenSize.width * 0.7 : screenSize.width * 0.95; final dialogHeight = isDesktop ? screenSize.height * 0.75 : screenSize.height * 0.85; return Dialog( backgroundColor: Colors.transparent, insetPadding: EdgeInsets.symmetric( horizontal: isDesktop ? 40 : 10, vertical: isDesktop ? 40 : 20, ), child: RawKeyboardListener( focusNode: FocusNode(), autofocus: true, onKey: (RawKeyEvent event) { if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) { Navigator.of(context).pop(); } }, child: Container( width: dialogWidth, height: dialogHeight, constraints: BoxConstraints( maxWidth: 900, maxHeight: 700, minWidth: 320, minHeight: 400, ), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Column( children: [ // 헤더 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), border: Border( bottom: BorderSide(color: Colors.grey.shade200), ), ), child: Row( children: [ Icon( Icons.history, color: Colors.grey.shade700, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '장비 이력', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( widget.equipmentName, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), overflow: TextOverflow.ellipsis, ), ], ), ), // 검색 필드 SizedBox( width: 200, height: 36, child: TextField( controller: _searchController, decoration: InputDecoration( hintText: '검색...', hintStyle: TextStyle(fontSize: 13), prefixIcon: const Icon(Icons.search, size: 18), filled: true, fillColor: Colors.grey.shade100, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), ), style: const TextStyle(fontSize: 13), ), ), const SizedBox(width: 8), // 새로고침 버튼 IconButton( icon: const Icon(Icons.refresh), onPressed: _isLoading ? null : () => _loadHistory(isRefresh: true), tooltip: '새로고침', ), // 닫기 버튼 IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), tooltip: '닫기', ), ], ), ), // 본문 Expanded( child: Builder( builder: (context) { // 초기 로딩 if ((_isInitialLoad || _isLoading) && _histories.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text('이력을 불러오는 중...'), ], ), ); } // 에러 상태 if (_error != null && _histories.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 48, color: Colors.red.shade400, ), const SizedBox(height: 16), Text( '오류 발생', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( _error!, textAlign: TextAlign.center, style: TextStyle( color: Colors.red.shade600, fontSize: 13, ), ), ), const SizedBox(height: 16), ElevatedButton.icon( icon: const Icon(Icons.refresh, size: 16), label: const Text('다시 시도'), onPressed: () => _loadHistory(isRefresh: true), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), ), ), ], ), ); } // 데이터 없음 if (_filteredHistories.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.inbox_outlined, size: 48, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( _searchQuery.isNotEmpty ? '검색 결과가 없습니다.' : '이력이 없습니다.', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), if (_searchQuery.isNotEmpty) ...[ const SizedBox(height: 8), TextButton( onPressed: () { _searchController.clear(); }, child: const Text('검색 초기화'), ), ], ], ), ); } // 이력 목록 return Padding( padding: const EdgeInsets.all(16), child: ListView.builder( controller: _scrollController, itemCount: _filteredHistories.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0), itemBuilder: (context, index) { if (index == _filteredHistories.length) { return const Padding( padding: EdgeInsets.all(16.0), child: Center( child: CircularProgressIndicator(), ), ); } return _buildHistoryItem(_filteredHistories[index]); }, ), ); }, ), ), // 푸터 Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '총 ${_histories.length}개 이력', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), if (_isLoading && _histories.isNotEmpty) Row( children: [ const SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 2, ), ), const SizedBox(width: 8), Text( '더 불러오는 중...', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), ], ), ), ], ), ), ), ); } }