import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:superport/screens/common/theme_shadcn.dart'; import 'package:superport/screens/common/components/shadcn_components.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/common/layouts/base_list_screen.dart'; import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart'; import 'package:superport/models/equipment_unified_model.dart'; import 'package:superport/utils/constants.dart'; import 'package:superport/utils/equipment_display_helper.dart'; import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart'; /// shadcn/ui 스타일로 재설계된 장비 관리 화면 class EquipmentList extends StatefulWidget { final String currentRoute; const EquipmentList({Key? key, this.currentRoute = Routes.equipment}) : super(key: key); @override State createState() => _EquipmentListState(); } class _EquipmentListState extends State { late final EquipmentListController _controller; bool _showDetailedColumns = true; final TextEditingController _searchController = TextEditingController(); final ScrollController _horizontalScrollController = ScrollController(); final ScrollController _scrollController = ScrollController(); String _selectedStatus = 'all'; // String _searchKeyword = ''; // Removed - unused field String _appliedSearchKeyword = ''; // 페이지 상태는 이제 Controller에서 관리 final Set _selectedItems = {}; @override void initState() { super.initState(); _controller = EquipmentListController(); _controller.pageSize = 10; // 페이지 크기 설정 _setInitialFilter(); // API 호출을 위해 Future로 변경 WidgetsBinding.instance.addPostFrameCallback((_) { _controller.loadData(); // 비동기 호출 }); } @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 > 900; }); } /// 라우트에 따른 초기 필터 설정 void _setInitialFilter() { switch (widget.currentRoute) { case Routes.equipmentInList: _selectedStatus = 'in'; _controller.selectedStatusFilter = EquipmentStatus.in_; break; case Routes.equipmentOutList: _selectedStatus = 'out'; _controller.selectedStatusFilter = EquipmentStatus.out; break; case Routes.equipmentRentList: _selectedStatus = 'rent'; _controller.selectedStatusFilter = EquipmentStatus.rent; break; default: _selectedStatus = 'all'; _controller.selectedStatusFilter = null; } print('DEBUG: Initial filter set - route: ${widget.currentRoute}, status: $_selectedStatus, filter: ${_controller.selectedStatusFilter}'); // 디버그 정보 } /// 데이터 로드 Future _loadData({bool isRefresh = false}) async { await _controller.loadData(isRefresh: isRefresh); } /// 상태 필터 변경 Future _onStatusFilterChanged(String status) async { setState(() { _selectedStatus = status; // 상태 필터를 EquipmentStatus 상수로 변환 switch (status) { case 'all': _controller.selectedStatusFilter = null; break; case 'in': _controller.selectedStatusFilter = EquipmentStatus.in_; break; case 'out': _controller.selectedStatusFilter = EquipmentStatus.out; break; case 'rent': _controller.selectedStatusFilter = EquipmentStatus.rent; break; case 'repair': _controller.selectedStatusFilter = EquipmentStatus.repair; break; case 'damaged': _controller.selectedStatusFilter = EquipmentStatus.damaged; break; case 'lost': _controller.selectedStatusFilter = EquipmentStatus.lost; break; case 'disposed': _controller.selectedStatusFilter = EquipmentStatus.disposed; break; default: _controller.selectedStatusFilter = null; } _controller.goToPage(1); }); _controller.changeStatusFilter(_controller.selectedStatusFilter); } /// 검색 실행 void _onSearch() async { setState(() { _appliedSearchKeyword = _searchController.text; _controller.goToPage(1); }); _controller.updateSearchKeyword(_searchController.text); } /// 장비 선택/해제 void _onEquipmentSelected(int? id, String status, bool? isSelected) { if (id == null) return; // UnifiedEquipment를 찾아서 선택/해제 UnifiedEquipment? equipment; try { equipment = _controller.items.firstWhere( (e) => e.equipment.id == id && e.status == status, ); } catch (e) { // 해당하는 장비를 찾지 못함 return; } setState(() { _controller.selectEquipment(equipment!); }); } /// 전체 선택/해제 void _onSelectAll(bool? value) { setState(() { final equipments = _getFilteredEquipments(); for (final equipment in equipments) { _controller.selectEquipment(equipment); } }); } /// 전체 선택 상태 확인 bool _isAllSelected() { final equipments = _getFilteredEquipments(); if (equipments.isEmpty) return false; return equipments.every((e) => _controller.selectedEquipmentIds.contains('${e.id}:${e.status}')); } /// 필터링된 장비 목록 반환 List _getFilteredEquipments() { // 서버에서 이미 페이지네이션된 데이터를 사용 var equipments = _controller.equipments; // 로컬 검색 키워드 적용 (서버 검색과 병행) // 서버에서 검색된 결과에 추가 로컬 필터링 if (_appliedSearchKeyword.isNotEmpty) { equipments = equipments.where((e) { final keyword = _appliedSearchKeyword.toLowerCase(); return [ e.equipment.manufacturer, e.equipment.name, e.equipment.category, e.equipment.subCategory, e.equipment.subSubCategory, e.equipment.serialNumber ?? '', e.equipment.barcode ?? '', e.equipment.remark ?? '', e.equipment.warrantyLicense ?? '', e.notes ?? '', ].any((field) => field.toLowerCase().contains(keyword)); }).toList(); } return equipments; } /// 출고 처리 버튼 핸들러 void _handleOutEquipment() async { if (_controller.getSelectedInStockCount() == 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('출고할 장비를 선택해주세요.')), ); return; } // 선택된 장비들의 요약 정보를 가져와서 출고 폼으로 전달 final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary(); final result = await Navigator.pushNamed( context, Routes.equipmentOutAdd, arguments: {'selectedEquipments': selectedEquipmentsSummary}, ); if (result == true) { setState(() { _controller.loadData(isRefresh: true); _controller.goToPage(1); }); } } /// 대여 처리 버튼 핸들러 void _handleRentEquipment() async { if (_controller.getSelectedInStockCount() == 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('대여할 장비를 선택해주세요.')), ); return; } final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${selectedEquipmentsSummary.length}개 장비 대여 기능은 준비 중입니다.'), ), ); } /// 폐기 처리 버튼 핸들러 void _handleDisposeEquipment() async { final selectedEquipments = _controller.getSelectedEquipments() .where((equipment) => equipment.status != EquipmentStatus.disposed) .toList(); if (selectedEquipments.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('폐기할 장비를 선택해주세요. (이미 폐기된 장비는 제외)')), ); return; } // 폐기 사유 입력을 위한 컨트롤러 final TextEditingController reasonController = TextEditingController(); final result = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('폐기 확인'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('선택한 ${selectedEquipments.length}개 장비를 폐기하시겠습니까?'), const SizedBox(height: 16), const Text('폐기할 장비 목록:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), ...selectedEquipments.map((unifiedEquipment) { final equipment = unifiedEquipment.equipment; return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( '${equipment.manufacturer} ${equipment.name}', style: const TextStyle(fontSize: 14), ), ); }), const SizedBox(height: 16), const Text('폐기 사유:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), TextField( controller: reasonController, decoration: const InputDecoration( hintText: '폐기 사유를 입력해주세요', border: OutlineInputBorder(), ), maxLines: 2, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('취소'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('폐기', style: TextStyle(color: Colors.red)), ), ], ), ); if (result == true) { // 로딩 다이얼로그 표시 showDialog( context: context, barrierDismissible: false, builder: (context) => const Center( child: CircularProgressIndicator(), ), ); try { await _controller.disposeSelectedEquipments( reason: reasonController.text.isNotEmpty ? reasonController.text : null, ); if (mounted) { Navigator.pop(context); // 로딩 다이얼로그 닫기 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('선택한 장비가 폐기 처리되었습니다.')), ); setState(() { _controller.loadData(isRefresh: true); }); } } catch (e) { if (mounted) { Navigator.pop(context); // 로딩 다이얼로그 닫기 ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('폐기 처리 실패: ${e.toString()}')), ); } } } reasonController.dispose(); } /// 편집 핸들러 void _handleEdit(UnifiedEquipment equipment) async { // 디버그: 실제 상태 값 확인 print('DEBUG: equipment.status = ${equipment.status}'); print('DEBUG: equipment.id = ${equipment.id}'); print('DEBUG: equipment.equipment.id = ${equipment.equipment.id}'); // 모든 상태의 장비 수정 가능 // equipment.equipment.id를 사용해야 실제 장비 ID임 final result = await Navigator.pushNamed( context, Routes.equipmentInEdit, arguments: equipment.equipment.id ?? equipment.id, // 실제 장비 ID 전달 ); if (result == true) { setState(() { _controller.loadData(isRefresh: true); _controller.goToPage(1); }); } } /// 삭제 핸들러 void _handleDelete(UnifiedEquipment equipment) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('삭제 확인'), content: const Text('이 장비 정보를 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('취소'), ), TextButton( onPressed: () async { Navigator.pop(context); // 로딩 다이얼로그 표시 showDialog( context: context, barrierDismissible: false, builder: (context) => const Center( child: CircularProgressIndicator(), ), ); // Controller를 통한 삭제 처리 await _controller.deleteEquipment(equipment.equipment.id!, equipment.status); // 로딩 다이얼로그 닫기 if (mounted) Navigator.pop(context); // 삭제 후 리스트 새로고침 (서버에서 10개 다시 가져오기) if (mounted) { setState(() { _controller.loadData(isRefresh: true); }); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('장비가 삭제되었습니다.')), ); } }, child: const Text('삭제', style: TextStyle(color: Colors.red)), ), ], ), ); } /// 이력 보기 핸들러 void _handleHistory(UnifiedEquipment equipment) async { if (equipment.equipment.id == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('장비 ID가 없습니다.')), ); return; } // 팝업 다이얼로그로 이력 표시 final result = await EquipmentHistoryDialog.show( context: context, equipmentId: equipment.equipment.id!, equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.name}', ); if (result == true) { _controller.loadData(isRefresh: true); } } @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, child: Consumer( builder: (context, controller, child) { // 선택된 장비 개수 final int selectedCount = controller.getSelectedEquipmentCount(); final int selectedInCount = controller.getSelectedInStockCount(); final int selectedOutCount = controller.getSelectedEquipmentCountByStatus(EquipmentStatus.out); final int selectedRentCount = controller.getSelectedEquipmentCountByStatus(EquipmentStatus.rent); final filteredEquipments = _getFilteredEquipments(); // 백엔드 API에서 제공하는 실제 전체 아이템 수 사용 final totalCount = controller.total; return BaseListScreen( isLoading: controller.isLoading && controller.equipments.isEmpty, error: controller.error, onRefresh: () => controller.loadData(isRefresh: true), emptyMessage: _appliedSearchKeyword.isNotEmpty ? '검색 결과가 없습니다' : '등록된 장비가 없습니다', emptyIcon: Icons.inventory_2_outlined, // 검색바 searchBar: _buildSearchBar(), // 액션바 actionBar: _buildActionBar(selectedCount, selectedInCount, selectedOutCount, selectedRentCount, totalCount), // 데이터 테이블 dataTable: _buildDataTable(filteredEquipments), // 페이지네이션 pagination: controller.totalPages > 1 ? Pagination( totalCount: controller.total, currentPage: controller.currentPage, pageSize: controller.pageSize, onPageChanged: (page) { controller.goToPage(page); }, ) : null, ); }, ), ); } /// 검색 바 Widget _buildSearchBar() { return Row( children: [ // 검색 입력 Expanded( flex: 2, child: Container( height: 40, decoration: BoxDecoration( color: ShadcnTheme.card, borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), border: Border.all(color: Colors.black), ), child: TextField( controller: _searchController, onSubmitted: (_) => _onSearch(), decoration: InputDecoration( hintText: '장비명, 제조사, 카테고리, 시리얼번호 등...', hintStyle: TextStyle(color: ShadcnTheme.mutedForeground.withValues(alpha: 0.8), fontSize: 14), prefixIcon: Icon(Icons.search, color: ShadcnTheme.muted, size: 20), border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), style: ShadcnTheme.bodyMedium, ), ), ), const SizedBox(width: 16), // 검색 버튼 SizedBox( height: 40, child: ShadcnButton( text: '검색', onPressed: _onSearch, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.search, size: 16), ), ), const SizedBox(width: 16), // 상태 필터 드롭다운 (캐시된 데이터 사용) Container( height: 40, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: ShadcnTheme.card, border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedStatus, onChanged: (value) => _onStatusFilterChanged(value!), style: TextStyle(fontSize: 14, color: ShadcnTheme.foreground), icon: const Icon(Icons.arrow_drop_down, size: 20), items: _buildStatusDropdownItems(), ), ), ), ], ); } /// 액션바 Widget _buildActionBar(int selectedCount, int selectedInCount, int selectedOutCount, int selectedRentCount, int totalCount) { return StandardActionBar( leftActions: [ // 라우트별 액션 버튼 _buildRouteSpecificActions(selectedInCount, selectedOutCount, selectedRentCount), ], rightActions: [ // 관리자용 비활성 포함 체크박스 // TODO: 실제 권한 체크 로직 추가 필요 Row( children: [ Checkbox( value: _controller.includeInactive, onChanged: (_) => setState(() { _controller.toggleIncludeInactive(); }), ), const Text('비활성 포함'), ], ), ], totalCount: totalCount, selectedCount: selectedCount, onRefresh: () { setState(() { _controller.loadData(); _controller.goToPage(1); }); }, statusMessage: _appliedSearchKeyword.isNotEmpty ? '"$_appliedSearchKeyword" 검색 결과' : null, ); } /// 라우트별 액션 버튼 Widget _buildRouteSpecificActions(int selectedInCount, int selectedOutCount, int selectedRentCount) { switch (widget.currentRoute) { case Routes.equipmentInList: return Row( children: [ ShadcnButton( text: '출고', onPressed: selectedInCount > 0 ? _handleOutEquipment : null, variant: selectedInCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.exit_to_app, size: 16), ), const SizedBox(width: 8), ShadcnButton( text: '입고', onPressed: () async { final result = await Navigator.pushNamed( context, Routes.equipmentInAdd, ); if (result == true) { setState(() { _controller.loadData(); _controller.goToPage(1); }); } }, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add, size: 16), ), ], ); case Routes.equipmentOutList: return Row( children: [ ShadcnButton( text: '재입고', onPressed: selectedOutCount > 0 ? () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('재입고 기능은 준비 중입니다.')), ) : null, variant: selectedOutCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.assignment_return, size: 16), ), const SizedBox(width: 8), ShadcnButton( text: '수리 요청', onPressed: selectedOutCount > 0 ? () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('수리 요청 기능은 준비 중입니다.')), ) : null, variant: selectedOutCount > 0 ? ShadcnButtonVariant.destructive : ShadcnButtonVariant.secondary, icon: const Icon(Icons.build, size: 16), ), ], ); case Routes.equipmentRentList: return Row( children: [ ShadcnButton( text: '반납', onPressed: selectedRentCount > 0 ? () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('대여 반납 기능은 준비 중입니다.')), ) : null, variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.keyboard_return, size: 16), ), const SizedBox(width: 8), ShadcnButton( text: '연장', onPressed: selectedRentCount > 0 ? () => ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('대여 연장 기능은 준비 중입니다.')), ) : null, variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.date_range, size: 16), ), ], ); default: return Row( children: [ ShadcnButton( text: '입고', onPressed: () async { final result = await Navigator.pushNamed( context, Routes.equipmentInAdd, ); if (result == true) { setState(() { _controller.loadData(); _controller.goToPage(1); }); } }, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add, size: 16), ), const SizedBox(width: 8), ShadcnButton( text: '출고 처리', onPressed: selectedInCount > 0 ? _handleOutEquipment : null, variant: selectedInCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, textColor: selectedInCount > 0 ? Colors.white : null, icon: const Icon(Icons.local_shipping, size: 16), ), const SizedBox(width: 8), ShadcnButton( text: '대여 처리', onPressed: selectedInCount > 0 ? _handleRentEquipment : null, variant: selectedInCount > 0 ? ShadcnButtonVariant.secondary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.assignment, size: 16), ), const SizedBox(width: 8), ShadcnButton( text: '폐기 처리', onPressed: selectedInCount > 0 ? _handleDisposeEquipment : null, variant: selectedInCount > 0 ? ShadcnButtonVariant.destructive : ShadcnButtonVariant.secondary, icon: const Icon(Icons.delete, size: 16), ), ], ); } } /// 최소 테이블 너비 계산 double _getMinimumTableWidth(List pagedEquipments) { double totalWidth = 0; // 기본 컬럼들 (최소 너비) totalWidth += 40; // 체크박스 totalWidth += 50; // 번호 totalWidth += 120; // 제조사 totalWidth += 120; // 장비명 totalWidth += 100; // 카테고리 totalWidth += 50; // 수량 totalWidth += 70; // 상태 totalWidth += 80; // 날짜 totalWidth += 90; // 관리 // 상세 컬럼들 (조건부) if (_showDetailedColumns) { totalWidth += 120; // 시리얼번호 totalWidth += 120; // 바코드 totalWidth += 120; // 현재 위치 totalWidth += 100; // 창고 위치 totalWidth += 100; // 점검일 } // padding 추가 (좌우 각 16px) totalWidth += 32; return totalWidth; } /// 헤더 셀 빌더 Widget _buildHeaderCell( String text, { required int flex, required bool useExpanded, required double minWidth, }) { final child = Container( alignment: Alignment.centerLeft, child: Text( text, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500), ), ); if (useExpanded) { return Expanded(flex: flex, child: child); } else { return SizedBox(width: minWidth, child: child); } } /// 데이터 셀 빌더 Widget _buildDataCell( Widget child, { required int flex, required bool useExpanded, required double minWidth, }) { final container = Container( alignment: Alignment.centerLeft, child: child, ); if (useExpanded) { return Expanded(flex: flex, child: container); } else { return SizedBox(width: minWidth, child: container); } } /// 유연한 테이블 빌더 Widget _buildFlexibleTable(List pagedEquipments, {required bool useExpanded}) { final hasOutOrRent = pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent ); return Column( children: [ // 테이블 헤더 Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: 10, ), decoration: BoxDecoration( color: ShadcnTheme.muted.withValues(alpha: 0.3), border: Border( bottom: BorderSide(color: Colors.black), ), ), child: Row( children: [ // 체크박스 _buildDataCell( Checkbox( value: _isAllSelected(), onChanged: _onSelectAll, ), flex: 1, useExpanded: useExpanded, minWidth: 40, ), // 번호 _buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 50), // 제조사 _buildHeaderCell('제조사', flex: 3, useExpanded: useExpanded, minWidth: 120), // 장비명 _buildHeaderCell('장비명', flex: 3, useExpanded: useExpanded, minWidth: 120), // 카테고리 _buildHeaderCell('카테고리', flex: 2, useExpanded: useExpanded, minWidth: 100), // 상세 정보 (조건부) if (_showDetailedColumns) ...[ _buildHeaderCell('시리얼번호', flex: 3, useExpanded: useExpanded, minWidth: 120), _buildHeaderCell('바코드', flex: 3, useExpanded: useExpanded, minWidth: 120), ], // 수량 _buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 50), // 상태 _buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70), // 날짜 _buildHeaderCell('날짜', flex: 2, useExpanded: useExpanded, minWidth: 80), // 상세 정보 (조건부) if (_showDetailedColumns) ...[ _buildHeaderCell('현재 위치', flex: 3, useExpanded: useExpanded, minWidth: 120), _buildHeaderCell('창고 위치', flex: 2, useExpanded: useExpanded, minWidth: 100), _buildHeaderCell('점검일', flex: 2, useExpanded: useExpanded, minWidth: 100), ], // 관리 _buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90), ], ), ), // 테이블 데이터 ...pagedEquipments.asMap().entries.map((entry) { final int index = entry.key; final UnifiedEquipment equipment = entry.value; return Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: 4, ), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: Colors.black), ), ), child: Row( children: [ // 체크박스 _buildDataCell( Checkbox( value: _selectedItems.contains(equipment.equipment.id ?? 0), onChanged: (bool? value) { if (equipment.equipment.id != null) { _onItemSelected(equipment.equipment.id!, value ?? false); } }, ), flex: 1, useExpanded: useExpanded, minWidth: 40, ), // 번호 _buildDataCell( Text( '${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}', style: ShadcnTheme.bodySmall, ), flex: 1, useExpanded: useExpanded, minWidth: 50, ), // 제조사 _buildDataCell( _buildTextWithTooltip( equipment.equipment.manufacturer, equipment.equipment.manufacturer, ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), // 장비명 _buildDataCell( _buildTextWithTooltip( equipment.equipment.name, equipment.equipment.name, ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), // 카테고리 _buildDataCell( _buildCategoryWithTooltip(equipment), flex: 2, useExpanded: useExpanded, minWidth: 100, ), // 상세 정보 (조건부) if (_showDetailedColumns) ...[ _buildDataCell( _buildTextWithTooltip( equipment.equipment.serialNumber ?? '-', equipment.equipment.serialNumber ?? '-', ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), _buildDataCell( _buildTextWithTooltip( equipment.equipment.barcode ?? '-', equipment.equipment.barcode ?? '-', ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), ], // 수량 _buildDataCell( Text( equipment.equipment.quantity.toString(), style: ShadcnTheme.bodySmall, ), flex: 1, useExpanded: useExpanded, minWidth: 50, ), // 상태 _buildDataCell( _buildStatusBadge(equipment.status), flex: 2, useExpanded: useExpanded, minWidth: 70, ), // 날짜 _buildDataCell( _buildDateWidget(equipment), flex: 2, useExpanded: useExpanded, minWidth: 80, ), // 상세 정보 (조건부) if (_showDetailedColumns) ...[ // 현재 위치 (회사 + 지점) _buildDataCell( _buildTextWithTooltip( _buildCurrentLocationText(equipment), _buildCurrentLocationText(equipment), ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), // 창고 위치 _buildDataCell( Text( equipment.warehouseLocation ?? '-', style: ShadcnTheme.bodySmall, ), flex: 2, useExpanded: useExpanded, minWidth: 100, ), // 점검일 (최근/다음) _buildDataCell( _buildInspectionDateWidget(equipment), flex: 2, useExpanded: useExpanded, minWidth: 100, ), ], // 관리 _buildDataCell( _buildActionButtons(equipment.equipment.id ?? 0), flex: 2, useExpanded: useExpanded, minWidth: 90, ), ], ), ); }).toList(), ], ); } /// 데이터 테이블 Widget _buildDataTable(List filteredEquipments) { // 백엔드에서 이미 페이지네이션된 데이터를 받으므로 // 프론트엔드에서 추가 페이징 불필요 final List pagedEquipments = filteredEquipments; // 전체 데이터가 없는지 확인 (API의 total 사용) if (_controller.total == 0 && pagedEquipments.isEmpty) { return StandardEmptyState( title: _appliedSearchKeyword.isNotEmpty ? '검색 결과가 없습니다' : '등록된 장비가 없습니다', icon: Icons.inventory_2_outlined, action: _appliedSearchKeyword.isEmpty ? StandardActionButtons.addButton( text: '첫 장비 추가하기', onPressed: () async { final result = await Navigator.pushNamed( context, Routes.equipmentInAdd, ); if (result == true) { setState(() { _controller.loadData(); _controller.goToPage(1); }); } }, ) : null, ); } return Container( width: double.infinity, decoration: BoxDecoration( border: Border.all(color: Colors.black), borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd), ), child: LayoutBuilder( builder: (context, constraints) { final availableWidth = constraints.maxWidth; final minimumWidth = _getMinimumTableWidth(pagedEquipments); final needsHorizontalScroll = minimumWidth > availableWidth; if (needsHorizontalScroll) { // 최소 너비보다 작을 때만 스크롤 활성화 return SingleChildScrollView( scrollDirection: Axis.horizontal, controller: _horizontalScrollController, child: SizedBox( width: minimumWidth, child: _buildFlexibleTable(pagedEquipments, useExpanded: false), ), ); } else { // 충분한 공간이 있을 때는 Expanded 사용 return _buildFlexibleTable(pagedEquipments, useExpanded: true); } }, ), ); } /// 텍스트와 툴팁 위젯 빌더 Widget _buildTextWithTooltip(String text, String tooltip) { return Tooltip( message: tooltip, child: Text( text, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ); } /// 상태 배지 빌더 Widget _buildStatusBadge(String status) { String displayText; ShadcnBadgeVariant variant; switch (status) { case EquipmentStatus.in_: displayText = '입고'; variant = ShadcnBadgeVariant.success; break; case EquipmentStatus.out: displayText = '출고'; variant = ShadcnBadgeVariant.destructive; break; case EquipmentStatus.rent: displayText = '대여'; variant = ShadcnBadgeVariant.warning; break; default: displayText = '알수없음'; variant = ShadcnBadgeVariant.secondary; } return ShadcnBadge( text: displayText, variant: variant, size: ShadcnBadgeSize.small, ); } /// 날짜 위젯 빌더 Widget _buildDateWidget(UnifiedEquipment equipment) { String dateStr = equipment.date.toString().substring(0, 10); return Text( dateStr, style: ShadcnTheme.bodySmall, ); } /// 액션 버튼 빌더 Widget _buildActionButtons(int equipmentId) { return Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: const Icon(Icons.history, size: 16), onPressed: () => _showEquipmentHistoryDialog(equipmentId), tooltip: '이력', ), ), Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: const Icon(Icons.edit_outlined, size: 16), onPressed: () => _handleEditById(equipmentId), tooltip: '편집', ), ), Flexible( child: IconButton( constraints: const BoxConstraints( minWidth: 30, minHeight: 30, ), padding: const EdgeInsets.all(4), icon: const Icon(Icons.delete_outline, size: 16), onPressed: () => _handleDeleteById(equipmentId), tooltip: '삭제', ), ), ], ); } // 장비 이력 다이얼로그 표시 void _showEquipmentHistoryDialog(int equipmentId) async { // 해당 장비 찾기 final equipment = _controller.equipments.firstWhere( (e) => e.equipment.id == equipmentId, orElse: () => throw Exception('Equipment not found'), ); // 팝업 다이얼로그로 이력 표시 final result = await EquipmentHistoryDialog.show( context: context, equipmentId: equipmentId, equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.name}', ); if (result == true) { _controller.loadData(isRefresh: true); } } // 편집 다이얼로그 표시 void _showEditDialog(UnifiedEquipment equipment) { _handleEdit(equipment); } // 삭제 다이얼로그 표시 void _showDeleteDialog(UnifiedEquipment equipment) { _handleDelete(equipment); } // 편집 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리 void _handleEditById(int equipmentId) { // 해당 장비 찾기 final equipment = _controller.equipments.firstWhere( (e) => e.equipment.id == equipmentId, orElse: () => throw Exception('Equipment not found'), ); _handleEdit(equipment); } // 삭제 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리 void _handleDeleteById(int equipmentId) { // 해당 장비 찾기 final equipment = _controller.equipments.firstWhere( (e) => e.equipment.id == equipmentId, orElse: () => throw Exception('Equipment not found'), ); _handleDelete(equipment); } /// 체크박스 선택 관련 함수들 void _onItemSelected(int id, bool selected) { setState(() { if (selected) { _selectedItems.add(id); } else { _selectedItems.remove(id); } }); } /// 페이지 데이터 가져오기 List _getPagedEquipments() { // 서버 페이지네이션 사용: 컨트롤러의 items가 이미 페이지네이션된 데이터 // 로컬 필터링만 적용 return _getFilteredEquipments(); } /// 카테고리 축약 표기 함수 String _shortenCategory(String category) { if (category.length <= 2) return category; return '${category.substring(0, 2)}...'; } /// 카테고리 툴팁 위젯 Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) { final fullCategory = EquipmentDisplayHelper.formatCategory( equipment.equipment.category, equipment.equipment.subCategory, equipment.equipment.subSubCategory, ); // 축약 표기 적용 - 비어있지 않은 카테고리만 표시 final List parts = []; if (equipment.equipment.category.isNotEmpty) { parts.add(_shortenCategory(equipment.equipment.category)); } if (equipment.equipment.subCategory.isNotEmpty) { parts.add(_shortenCategory(equipment.equipment.subCategory)); } if (equipment.equipment.subSubCategory.isNotEmpty) { parts.add(_shortenCategory(equipment.equipment.subSubCategory)); } final shortCategory = parts.join(' > '); return Tooltip( message: fullCategory, child: Text( shortCategory, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ); } /// 캐시된 데이터를 사용한 상태 드롭다운 아이템 생성 List> _buildStatusDropdownItems() { List> items = [ const DropdownMenuItem(value: 'all', child: Text('전체')), ]; // 캐시된 상태 데이터에서 드롭다운 아이템 생성 final cachedStatuses = _controller.getCachedEquipmentStatuses(); for (final status in cachedStatuses) { items.add( DropdownMenuItem( value: status.id, child: Text(status.name), ), ); } // 캐시된 데이터가 없을 때 폴백으로 하드코딩된 상태 사용 if (cachedStatuses.isEmpty) { items.addAll([ const DropdownMenuItem(value: 'in', child: Text('입고')), const DropdownMenuItem(value: 'out', child: Text('출고')), const DropdownMenuItem(value: 'rent', child: Text('대여')), const DropdownMenuItem(value: 'repair', child: Text('수리중')), const DropdownMenuItem(value: 'damaged', child: Text('손상')), const DropdownMenuItem(value: 'lost', child: Text('분실')), const DropdownMenuItem(value: 'disposed', child: Text('폐기')), ]); } return items; } /// 현재 위치 텍스트 생성 (회사명 + 지점명) String _buildCurrentLocationText(UnifiedEquipment equipment) { final currentCompany = equipment.currentCompany ?? '-'; final currentBranch = equipment.currentBranch ?? ''; if (currentBranch.isNotEmpty) { return '$currentCompany ($currentBranch)'; } else { return currentCompany; } } /// 점검일 위젯 생성 (최근 점검일/다음 점검일) Widget _buildInspectionDateWidget(UnifiedEquipment equipment) { final lastInspection = equipment.lastInspectionDate; final nextInspection = equipment.nextInspectionDate; String displayText = '-'; Color? textColor; if (nextInspection != null) { final now = DateTime.now(); final difference = nextInspection.difference(now).inDays; if (difference < 0) { displayText = '점검 필요'; textColor = Colors.red; } else if (difference <= 30) { displayText = '${difference}일 후'; textColor = Colors.orange; } else { displayText = '${nextInspection.month}/${nextInspection.day}'; textColor = Colors.green; } } else if (lastInspection != null) { displayText = '${lastInspection.month}/${lastInspection.day}'; } return Text( displayText, style: ShadcnTheme.bodySmall.copyWith( color: textColor ?? ShadcnTheme.bodySmall.color, ), ); } }