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/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/core/constants/app_constants.dart'; import 'package:superport/utils/constants.dart'; import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart'; import 'package:superport/screens/equipment/widgets/equipment_search_dialog.dart'; import 'package:superport/screens/equipment/dialogs/equipment_outbound_dialog.dart'; import 'package:superport/data/models/equipment/equipment_dto.dart'; import 'package:superport/domain/usecases/equipment/get_equipment_detail_usecase.dart'; import 'package:get_it/get_it.dart'; import 'package:superport/data/repositories/equipment_history_repository.dart'; import 'package:superport/data/models/stock_status_dto.dart'; import 'package:superport/data/datasources/remote/api_client.dart'; /// shadcn/ui 스타일로 재설계된 장비 관리 화면 class EquipmentList extends StatefulWidget { final String currentRoute; const EquipmentList({super.key, this.currentRoute = Routes.equipment}); @override State createState() => _EquipmentListState(); } class _EquipmentListState extends State { late final EquipmentListController _controller; bool _showDetailedColumns = true; final TextEditingController _searchController = TextEditingController(); final ScrollController _horizontalScrollController = ScrollController(); String _selectedStatus = 'all'; // String _searchKeyword = ''; // Removed - unused field String _appliedSearchKeyword = ''; // 페이지 상태는 이제 Controller에서 관리 final Set _selectedItems = {}; Map? _cachedDropdownData; // 드롭다운 데이터 캐시 @override void initState() { super.initState(); _controller = EquipmentListController(); _controller.pageSize = AppConstants.equipmentPageSize; // 페이지 크기 설정 _setInitialFilter(); _preloadDropdownData(); // 드롭다운 데이터 미리 로드 // API 호출을 위해 Future로 변경 WidgetsBinding.instance.addPostFrameCallback((_) { _controller.loadData(); // 비동기 호출 }); } // 드롭다운 데이터를 미리 로드하는 메서드 Future _preloadDropdownData() async { try { await _controller.preloadDropdownData(); if (mounted) { setState(() { _cachedDropdownData = _controller.cachedDropdownData; }); } } catch (e) { print('Failed to preload dropdown data: $e'); } } @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(() { // 1200px 이상에서만 상세 컬럼 (바코드, 구매가격, 구매일, 보증기간) 표시 _showDetailedColumns = width > 1200; }); } /// ShadTable 기반 장비 목록 테이블 /// /// - 표준 컴포넌트 사용으로 일관성 확보 /// - 핵심 컬럼만 우선 도입 (상태/장비번호/시리얼/제조사/모델/회사/창고/일자/관리) /// - 반응형: 가용 너비에 따라 일부 컬럼은 숨김 처리 가능 Widget _buildShadTable(List items, {required double availableWidth}) { final allSelected = items.isNotEmpty && items.every((e) => _selectedItems.contains(e.equipment.id)); return ShadTable.list( header: [ // 선택 ShadTableCell.header( child: ShadCheckbox( value: allSelected, onChanged: (checked) { setState(() { if (checked == true) { _selectedItems ..clear() ..addAll(items.map((e) => e.equipment.id).whereType()); } else { _selectedItems.clear(); } }); }, ), ), ShadTableCell.header(child: const Text('상태')), ShadTableCell.header(child: const Text('장비번호')), ShadTableCell.header(child: const Text('시리얼')), ShadTableCell.header(child: const Text('제조사')), ShadTableCell.header(child: const Text('모델')), if (availableWidth > 900) ShadTableCell.header(child: const Text('회사')), if (availableWidth > 1100) ShadTableCell.header(child: const Text('창고')), if (availableWidth > 800) ShadTableCell.header(child: const Text('일자')), ShadTableCell.header(child: const Text('관리')), ], children: items.map((item) { final id = item.equipment.id; final selected = id != null && _selectedItems.contains(id); return [ // 선택 체크박스 ShadTableCell( child: ShadCheckbox( value: selected, onChanged: (checked) { setState(() { if (id == null) return; if (checked == true) { _selectedItems.add(id); } else { _selectedItems.remove(id); } }); }, ), ), // 상태 ShadTableCell(child: _buildStatusBadge(item.status)), // 장비번호 ShadTableCell( child: _buildTextWithTooltip( item.equipment.equipmentNumber, item.equipment.equipmentNumber, ), ), // 시리얼 ShadTableCell( child: _buildTextWithTooltip( item.equipment.serialNumber ?? '-', item.equipment.serialNumber ?? '-', ), ), // 제조사 ShadTableCell( child: _buildTextWithTooltip( item.vendorName ?? item.equipment.manufacturer, item.vendorName ?? item.equipment.manufacturer, ), ), // 모델 ShadTableCell( child: _buildTextWithTooltip( item.modelName ?? item.equipment.modelName, item.modelName ?? item.equipment.modelName, ), ), // 회사 (반응형) if (availableWidth > 900) ShadTableCell( child: _buildTextWithTooltip( item.companyName ?? item.currentCompany ?? '-', item.companyName ?? item.currentCompany ?? '-', ), ), // 창고 (반응형) if (availableWidth > 1100) ShadTableCell( child: _buildTextWithTooltip( item.warehouseLocation ?? '-', item.warehouseLocation ?? '-', ), ), // 일자 (반응형) if (availableWidth > 800) ShadTableCell( child: _buildTextWithTooltip( _formatDate(item.date), _formatDate(item.date), ), ), // 관리 액션 ShadTableCell( child: Row( mainAxisSize: MainAxisSize.min, children: [ Tooltip( message: '이력 보기', child: ShadButton.ghost( size: ShadButtonSize.sm, onPressed: () => _showEquipmentHistoryDialog(item.equipment.id ?? 0), child: const Icon(Icons.history, size: 16), ), ), const SizedBox(width: 4), Tooltip( message: '수정', child: ShadButton.ghost( size: ShadButtonSize.sm, onPressed: () => _handleEdit(item), child: const Icon(Icons.edit, size: 16), ), ), const SizedBox(width: 4), Tooltip( message: '삭제', child: ShadButton.ghost( size: ShadButtonSize.sm, onPressed: () => _handleDelete(item), child: const Icon(Icons.delete_outline, size: 16), ), ), ], ), ), ]; }).toList(), ); } /// 라우트에 따른 초기 필터 설정 void _setInitialFilter() { switch (widget.currentRoute) { case Routes.equipmentInList: _selectedStatus = 'in'; _controller.selectedStatusFilter = 'I'; // 영문 코드 사용 break; case Routes.equipmentOutList: _selectedStatus = 'out'; _controller.selectedStatusFilter = 'O'; // 영문 코드 사용 break; case Routes.equipmentRentList: _selectedStatus = 'rent'; _controller.selectedStatusFilter = 'T'; // 영문 코드 사용 break; default: _selectedStatus = 'all'; _controller.selectedStatusFilter = null; } print('DEBUG: Initial filter set - route: ${widget.currentRoute}, status: $_selectedStatus, filter: ${_controller.selectedStatusFilter}'); // 디버그 정보 } /// 상태 필터 변경 Future _onStatusFilterChanged(String status) async { setState(() { _selectedStatus = status; // 상태 필터를 영문 코드로 변환 switch (status) { case 'all': _controller.selectedStatusFilter = null; break; case 'in': _controller.selectedStatusFilter = 'I'; break; case 'out': _controller.selectedStatusFilter = 'O'; break; case 'rent': _controller.selectedStatusFilter = 'T'; break; case 'repair': _controller.selectedStatusFilter = 'R'; break; case 'damaged': _controller.selectedStatusFilter = 'D'; break; case 'lost': _controller.selectedStatusFilter = 'L'; break; case 'disposed': _controller.selectedStatusFilter = 'P'; break; default: _controller.selectedStatusFilter = null; } _controller.goToPage(1); }); _controller.changeStatusFilter(_controller.selectedStatusFilter); } /// 회사 필터 변경 Future _onCompanyFilterChanged(int? companyId) async { setState(() { _controller.filterByCompany(companyId); _controller.goToPage(1); }); } /// 검색 실행 void _onSearch() async { setState(() { _appliedSearchKeyword = _searchController.text; _controller.goToPage(1); }); _controller.updateSearchKeyword(_searchController.text); } /// 필터링된 장비 목록 반환 List _getFilteredEquipments() { // 서버에서 이미 페이지네이션된 데이터를 사용 var equipments = _controller.equipments; // 로컬 검색 키워드 적용 (서버 검색과 병행) // 서버에서 검색된 결과에 추가 로컬 필터링 if (_appliedSearchKeyword.isNotEmpty) { equipments = equipments.where((e) { final keyword = _appliedSearchKeyword.toLowerCase(); return [ e.vendorName ?? '', // 백엔드 직접 제공 Vendor 이름 e.modelName ?? '', // 백엔드 직접 제공 Model 이름 e.companyName ?? '', // 백엔드 직접 제공 Company 이름 e.equipment.serialNumber ?? '', // 시리얼 번호 e.equipment.barcode ?? '', // 바코드 e.equipment.remark ?? '', // 비고 ].any((field) => field.toLowerCase().contains(keyword.toLowerCase())); }).toList(); } return equipments; } /// 출고 처리 버튼 핸들러 void _handleOutEquipment() async { if (_controller.getSelectedInStockCount() == 0) { ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('출고할 장비를 선택해주세요.'), ), ); return; } // ✅ 장비 수정과 동일한 방식: GetEquipmentDetailUseCase를 사용해서 완전한 데이터 로드 final selectedEquipmentIds = _controller.getSelectedEquipments() .where((e) => e.status == 'I') // 영문 코드로 통일 .map((e) => e.equipment.id) .where((id) => id != null) .cast() .toList(); print('[EquipmentList] Loading complete equipment details for ${selectedEquipmentIds.length} equipments using GetEquipmentDetailUseCase'); // ✅ stock-status API를 사용해서 실제 현재 창고 정보가 포함된 데이터 로드 final selectedEquipments = []; final equipmentHistoryRepository = EquipmentHistoryRepositoryImpl(GetIt.instance().dio); // stock-status API를 시도하되, 실패해도 출고 프로세스 계속 진행 Map stockStatusMap = {}; try { // 1. 모든 재고 상태 정보를 한 번에 로드 (실패해도 계속 진행) print('[EquipmentList] Attempting to load stock status...'); final stockStatusList = await equipmentHistoryRepository.getStockStatus(); for (final status in stockStatusList) { stockStatusMap[status.equipmentId] = status; } print('[EquipmentList] Stock status loaded successfully: ${stockStatusMap.length} items'); } catch (e) { print('[EquipmentList] ⚠️ Stock status API failed, continuing with basic equipment data: $e'); // 경고 메시지만 표시하고 계속 진행 ShadToaster.of(context).show(ShadToast( title: const Text('알림'), description: const Text('실시간 창고 정보를 가져올 수 없어 기본 정보로 진행합니다.'), )); } // 2. 각 장비의 상세 정보를 로드하고 가능하면 창고 정보를 매핑 final getEquipmentDetailUseCase = GetIt.instance(); for (final equipmentId in selectedEquipmentIds) { print('[EquipmentList] Loading details for equipment $equipmentId'); final result = await getEquipmentDetailUseCase(equipmentId); result.fold( (failure) { print('[EquipmentList] Failed to load equipment $equipmentId: ${failure.message}'); ShadToaster.of(context).show(ShadToast( title: const Text('오류'), description: Text('장비 정보를 불러오는데 실패했습니다: ${failure.message}'), )); return; // 실패 시 종료 }, (equipment) { // ✅ stock-status가 있으면 실제 창고 정보로 업데이트, 없으면 기존 정보 사용 final stockStatus = stockStatusMap[equipmentId]; EquipmentDto updatedEquipment = equipment; if (stockStatus != null) { updatedEquipment = equipment.copyWith( warehousesId: stockStatus.warehouseId, warehousesName: stockStatus.warehouseName, ); print('[EquipmentList] ===== REAL WAREHOUSE DATA ====='); print('[EquipmentList] Equipment ID: $equipmentId'); print('[EquipmentList] Serial Number: ${equipment.serialNumber}'); print('[EquipmentList] REAL Warehouse ID: ${stockStatus.warehouseId}'); print('[EquipmentList] REAL Warehouse Name: ${stockStatus.warehouseName}'); print('[EquipmentList] ====================================='); } else { print('[EquipmentList] ⚠️ No stock status found for equipment $equipmentId, using basic warehouse info'); print('[EquipmentList] Basic Warehouse ID: ${equipment.warehousesId}'); print('[EquipmentList] Basic Warehouse Name: ${equipment.warehousesName}'); } selectedEquipments.add(updatedEquipment); }, ); } // 모든 장비 정보를 성공적으로 로드했는지 확인 if (selectedEquipments.length != selectedEquipmentIds.length) { print('[EquipmentList] Failed to load complete equipment information'); return; // 일부 장비 정보 로드 실패 시 중단 } // 출고 다이얼로그 표시 final result = await showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return EquipmentOutboundDialog( selectedEquipments: selectedEquipments, ); }, ); if (result == true) { // 선택 상태 초기화 및 데이터 새로고침 _controller.clearSelection(); _controller.loadData(isRefresh: true); } } /// 대여 처리 버튼 핸들러 void _handleRentEquipment() async { if (_controller.getSelectedInStockCount() == 0) { ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('대여할 장비를 선택해주세요.'), ), ); return; } final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary(); ShadToaster.of(context).show( ShadToast( title: const Text('알림'), description: Text('${selectedEquipmentsSummary.length}개 장비 대여 기능은 준비 중입니다.'), ), ); } /// 폐기 처리 버튼 핸들러 void _handleDisposeEquipment() async { final selectedEquipments = _controller.getSelectedEquipments() .where((equipment) => equipment.status != 'P') // 영문 코드로 통일 .toList(); if (selectedEquipments.isEmpty) { ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('폐기할 장비를 선택해주세요. (이미 폐기된 장비는 제외)'), ), ); return; } // 폐기 사유 입력을 위한 컨트롤러 final TextEditingController reasonController = TextEditingController(); final result = await showShadDialog( context: context, builder: (context) => ShadDialog( title: const Text('폐기 확인'), description: 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( '${unifiedEquipment.vendorName ?? 'N/A'} ${equipment.serialNumber}', // 백엔드 직접 제공 Vendor + Equipment Number style: const TextStyle(fontSize: 14), ), ); }), const SizedBox(height: 16), const Text('폐기 사유:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), ShadInputFormField( controller: reasonController, placeholder: const Text('폐기 사유를 입력해주세요'), maxLines: 2, ), ], ), ), actions: [ ShadButton( onPressed: () => Navigator.pop(context, false), child: const Text('취소'), ), ShadButton.destructive( onPressed: () => Navigator.pop(context, true), child: const Text('폐기'), ), ], ), ); if (result == true) { // 로딩 다이얼로그 표시 showShadDialog( context: context, barrierDismissible: false, builder: (context) => const ShadDialog( child: Center( child: ShadProgress(), ), ), ); try { await _controller.disposeSelectedEquipments( reason: reasonController.text.isNotEmpty ? reasonController.text : null, ); if (mounted) { Navigator.pop(context); // 로딩 다이얼로그 닫기 ShadToaster.of(context).show( const ShadToast( title: Text('폐기 완료'), description: Text('선택한 장비가 폐기 처리되었습니다.'), ), ); setState(() { _controller.loadData(isRefresh: true); }); } } catch (e) { if (mounted) { Navigator.pop(context); // 로딩 다이얼로그 닫기 ShadToaster.of(context).show( ShadToast.destructive( title: const Text('폐기 실패'), description: Text(e.toString()), ), ); } } } reasonController.dispose(); } /// 드롭다운 데이터 확인 및 로드 Future> _ensureDropdownData() async { // 캐시된 데이터가 있으면 반환 if (_cachedDropdownData != null) { return _cachedDropdownData!; } // 없으면 새로 로드 await _preloadDropdownData(); return _cachedDropdownData ?? {}; } /// 편집 핸들러 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}'); // 로딩 다이얼로그 표시 showShadDialog( context: context, barrierDismissible: false, builder: (context) => ShadDialog( child: Container( padding: const EdgeInsets.all(24), child: const Column( mainAxisSize: MainAxisSize.min, children: [ ShadProgress(), SizedBox(height: 16), Text('장비 정보를 불러오는 중...'), ], ), ), ), ); try { // 장비 상세 데이터와 드롭다운 데이터를 병렬로 로드 final results = await Future.wait([ _controller.loadEquipmentDetail(equipment.equipment.id!), _ensureDropdownData(), ]); final equipmentDetail = results[0]; final dropdownData = results[1] as Map; // 로딩 다이얼로그 닫기 if (mounted) { Navigator.pop(context); } if (equipmentDetail == null) { if (mounted) { showShadDialog( context: context, builder: (context) => ShadDialog.alert( title: const Text('오류'), description: const Text('장비 정보를 불러올 수 없습니다.'), actions: [ ShadButton( child: const Text('확인'), onPressed: () => Navigator.pop(context), ), ], ), ); } return; } // 모든 데이터를 arguments로 전달 final result = await Navigator.pushNamed( context, Routes.equipmentInEdit, arguments: { 'equipmentId': equipment.equipment.id, 'equipment': equipmentDetail, 'dropdownData': dropdownData, }, ); if (result == true) { setState(() { _controller.loadData(isRefresh: true); _controller.goToPage(1); }); } } catch (e) { // 오류 발생 시 로딩 다이얼로그 닫기 if (mounted) { Navigator.pop(context); ShadToaster.of(context).show( ShadToast.destructive( title: const Text('오류'), description: Text('장비 정보를 불러올 수 없습니다: $e'), ), ); } } } /// 삭제 핸들러 void _handleDelete(UnifiedEquipment equipment) { showShadDialog( context: context, builder: (context) => ShadDialog( title: const Text('삭제 확인'), description: const Text('이 장비 정보를 삭제하시겠습니까?'), actions: [ ShadButton( onPressed: () => Navigator.pop(context), child: const Text('취소'), ), ShadButton( onPressed: () async { Navigator.pop(context); try { // Controller를 통한 삭제 처리 (내부에서 refresh() 호출) await _controller.deleteEquipment(equipment.equipment.id!, equipment.status); if (mounted) { ShadToaster.of(context).show( ShadToast( title: const Text('장비 삭제'), description: const Text('장비가 삭제되었습니다.'), ), ); } } catch (e) { if (mounted) { ShadToaster.of(context).show( ShadToast.destructive( title: const Text('삭제 실패'), description: Text(e.toString()), ), ); } } }, child: const Text('삭제'), ), ], ), ); } @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; // 디버그: 페이지네이션 상태 확인 print('DEBUG Pagination: total=${controller.total}, totalPages=${controller.totalPages}, pageSize=${controller.pageSize}, currentPage=${controller.currentPage}'); 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.total > controller.pageSize ? 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), // 상태 필터 드롭다운 (캐시된 데이터 사용) SizedBox( height: 40, width: 150, child: ShadSelect( selectedOptionBuilder: (context, value) => Text( _getStatusDisplayText(value), style: const TextStyle(fontSize: 14), ), placeholder: const Text('상태 선택'), options: _buildStatusSelectOptions(), onChanged: (value) { if (value != null) { _onStatusFilterChanged(value); } }, ), ), const SizedBox(width: 16), // 회사별 필터 드롭다운 SizedBox( height: 40, width: 150, child: ShadSelect( selectedOptionBuilder: (context, value) => Text( value == null ? '전체 회사' : _getCompanyDisplayText(value), style: const TextStyle(fontSize: 14), ), placeholder: const Text('회사 선택'), options: _buildCompanySelectOptions(), onChanged: (value) { _onCompanyFilterChanged(value); }, ), ), ], ); } /// 액션바 Widget _buildActionBar(int selectedCount, int selectedInCount, int selectedOutCount, int selectedRentCount, int totalCount) { return StandardActionBar( leftActions: [ // 라우트별 액션 버튼 _buildRouteSpecificActions(selectedInCount, selectedOutCount, selectedRentCount), const SizedBox(width: 8), // 검색 버튼 추가 ShadButton.outline( onPressed: () => _showEquipmentSearchDialog(), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.search, size: 16), const SizedBox(width: 4), const Text('고급 검색'), ], ), ), ], rightActions: [ // 관리자용 비활성 포함 체크박스 // TODO: 실제 권한 체크 로직 추가 필요 Row( children: [ ShadCheckbox( value: _controller.includeInactive, onChanged: (_) => setState(() { _controller.toggleIncludeInactive(); }), ), const SizedBox(width: 8), 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 Wrap( spacing: 8, runSpacing: 4, children: [ ShadcnButton( text: '출고', onPressed: selectedInCount > 0 ? _handleOutEquipment : null, variant: selectedInCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.exit_to_app, size: 16), ), ShadcnButton( text: '입고', onPressed: () async { final result = await Navigator.pushNamed( context, Routes.equipmentInAdd, ); if (result == true) { // 입고 완료 후 데이터 새로고침 (중복 방지) _controller.refresh(); } }, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add, size: 16), ), ], ); case Routes.equipmentOutList: return Wrap( spacing: 8, runSpacing: 4, children: [ ShadcnButton( text: '재입고', onPressed: selectedOutCount > 0 ? () => ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('재입고 기능은 준비 중입니다.'), ), ) : null, variant: selectedOutCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.assignment_return, size: 16), ), ShadcnButton( text: '수리', onPressed: selectedOutCount > 0 ? () => ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('수리 요청 기능은 준비 중입니다.'), ), ) : null, variant: selectedOutCount > 0 ? ShadcnButtonVariant.destructive : ShadcnButtonVariant.secondary, icon: const Icon(Icons.build, size: 16), ), ], ); case Routes.equipmentRentList: return Wrap( spacing: 8, runSpacing: 4, children: [ ShadcnButton( text: '반납', onPressed: selectedRentCount > 0 ? () => ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('대여 반납 기능은 준비 중입니다.'), ), ) : null, variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.keyboard_return, size: 16), ), ShadcnButton( text: '연장', onPressed: selectedRentCount > 0 ? () => ShadToaster.of(context).show( const ShadToast( title: Text('알림'), description: Text('대여 연장 기능은 준비 중입니다.'), ), ) : null, variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.date_range, size: 16), ), ], ); default: return Wrap( spacing: 8, runSpacing: 4, children: [ ShadcnButton( text: '입고', onPressed: () async { final result = await Navigator.pushNamed( context, Routes.equipmentInAdd, ); if (result == true) { // 입고 완료 후 데이터 새로고침 (중복 방지) _controller.refresh(); } }, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add, size: 16), ), 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), ), ShadcnButton( text: '대여', onPressed: selectedInCount > 0 ? _handleRentEquipment : null, variant: selectedInCount > 0 ? ShadcnButtonVariant.secondary : ShadcnButtonVariant.secondary, icon: const Icon(Icons.assignment, size: 16), ), 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 availableWidth) { double totalWidth = 0; // 필수 컬럼들 (항상 표시) - 더 작게 조정 totalWidth += 30; // 체크박스 (35->30) totalWidth += 35; // 번호 (40->35) totalWidth += 70; // 회사명 (90->70) totalWidth += 60; // 제조사 (80->60) totalWidth += 80; // 모델명 (100->80) totalWidth += 70; // 장비번호 (90->70) totalWidth += 50; // 상태 (60->50) totalWidth += 100; // 관리 (120->90->100, 아이콘 3개 수용) // 중간 화면용 추가 컬럼들 (800px 이상) if (availableWidth > 800) { totalWidth += 35; // 수량 (40->35) totalWidth += 70; // 입출고일 (80->70) } // 상세 컬럼들 (1200px 이상에서만 표시) if (_showDetailedColumns && availableWidth > 1200) { totalWidth += 70; // 바코드 (90->70) totalWidth += 70; // 구매가격 (80->70) totalWidth += 70; // 구매일 (80->70) totalWidth += 80; // 보증기간 (90->80) } // padding 추가 (좌우 각 2px로 축소) totalWidth += 4; return totalWidth; } /// 데이터 테이블 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, availableWidth); final needsHorizontalScroll = minimumWidth > availableWidth; // ShadTable 경로로 일괄 전환 (가로 스크롤은 ShadTable 외부에서 처리) if (needsHorizontalScroll) { return SingleChildScrollView( scrollDirection: Axis.horizontal, controller: _horizontalScrollController, child: SizedBox( width: minimumWidth, child: _buildShadTable(pagedEquipments, availableWidth: availableWidth), ), ); } else { return _buildShadTable(pagedEquipments, availableWidth: availableWidth); } }, ), ); } /// 텍스트와 툴팁 위젯 빌더 Widget _buildTextWithTooltip(String text, String tooltip) { return Tooltip( message: tooltip, child: Text( text, style: ShadcnTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ); } /// 가격 포맷팅 /// 날짜 포맷팅 String _formatDate(DateTime? date) { if (date == null) return '-'; return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } /// 보증기간 포맷팅 /// 상태 배지 빌더 Widget _buildStatusBadge(String status) { String displayText; ShadcnBadgeVariant variant; // 영문 코드만 사용 (EquipmentStatus 상수들도 실제로는 'I', 'O' 등의 값) switch (status) { case 'I': displayText = '입고'; variant = ShadcnBadgeVariant.success; break; case 'O': displayText = '출고'; variant = ShadcnBadgeVariant.destructive; break; case 'T': displayText = '대여'; variant = ShadcnBadgeVariant.warning; break; case 'R': displayText = '수리'; variant = ShadcnBadgeVariant.secondary; break; case 'D': displayText = '손상'; variant = ShadcnBadgeVariant.destructive; break; case 'L': displayText = '분실'; variant = ShadcnBadgeVariant.destructive; break; case 'P': displayText = '폐기'; variant = ShadcnBadgeVariant.secondary; break; default: displayText = '알수없음'; variant = ShadcnBadgeVariant.secondary; } return ShadcnBadge( text: displayText, variant: variant, size: ShadcnBadgeSize.small, ); } // 장비 이력 다이얼로그 표시 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.vendorName ?? 'N/A'} ${equipment.equipment.serialNumber}', // 백엔드 직접 제공 Vendor + Equipment Number ); if (result == true) { _controller.loadData(isRefresh: true); } } // 편집 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리 /// 체크박스 선택 관련 함수들 // 사용하지 않는 카테고리 관련 함수들 제거됨 (리스트 API에서 제공하지 않음) /// 상태 표시 텍스트 가져오기 String _getStatusDisplayText(String status) { switch (status) { case 'all': return '전체'; case 'in': return '입고'; case 'out': return '출고'; case 'rent': return '대여'; case 'repair': return '수리중'; case 'damaged': return '손상'; case 'lost': return '분실'; case 'disposed': return '폐기'; default: return '전체'; } } /// 캐시된 데이터를 사용한 상태 선택 옵션 생성 List> _buildStatusSelectOptions() { List> options = [ const ShadOption(value: 'all', child: Text('전체')), ]; // 캐시된 상태 데이터에서 선택 옵션 생성 final cachedStatuses = _controller.getCachedEquipmentStatuses(); for (final status in cachedStatuses) { options.add( ShadOption( value: status.id, child: Text(status.name), ), ); } // 캐시된 데이터가 없을 때 폴백으로 하드코딩된 상태 사용 if (cachedStatuses.isEmpty) { options.addAll([ const ShadOption(value: 'in', child: Text('입고')), const ShadOption(value: 'out', child: Text('출고')), const ShadOption(value: 'rent', child: Text('대여')), const ShadOption(value: 'repair', child: Text('수리중')), const ShadOption(value: 'damaged', child: Text('손상')), const ShadOption(value: 'lost', child: Text('분실')), const ShadOption(value: 'disposed', child: Text('폐기')), ]); } return options; } /// 회사명 표시 텍스트 가져오기 String _getCompanyDisplayText(int companyId) { // 캐시된 드롭다운 데이터에서 회사명 찾기 if (_cachedDropdownData != null && _cachedDropdownData!['companies'] != null) { final companies = _cachedDropdownData!['companies'] as List; for (final company in companies) { if (company['id'] == companyId) { return company['name'] ?? '알수없는 회사'; } } } return '회사 #$companyId'; } /// 소유회사별 필터 드롭다운 옵션 생성 List> _buildCompanySelectOptions() { List> options = [ const ShadOption(value: null, child: Text('전체 소유회사')), ]; // 캐시된 드롭다운 데이터에서 회사 목록 가져오기 if (_cachedDropdownData != null && _cachedDropdownData!['companies'] != null) { final companies = _cachedDropdownData!['companies'] as List; for (final company in companies) { final id = company['id'] as int?; final name = company['name'] as String?; if (id != null && name != null) { options.add( ShadOption( value: id, child: Text(name), ), ); } } } return options; } // 사용하지 않는 현재위치, 점검일 관련 함수들 제거됨 (리스트 API에서 제공하지 않음) /// 장비 고급 검색 다이얼로그 표시 void _showEquipmentSearchDialog() { showDialog( context: context, barrierDismissible: false, builder: (context) => EquipmentSearchDialog( onEquipmentFound: (equipment) { // 검색된 장비를 상세보기로 이동 또는 다른 처리 ShadToaster.of(context).show( ShadToast( title: const Text('장비 검색 완료'), description: Text('${equipment.serialNumber} 장비를 찾았습니다.'), ), ); // 필요하면 검색된 장비의 상세정보로 이동 // _onEditTap(equipment); }, ), ); } }