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/utils/constants.dart'; import 'package:superport/screens/equipment/widgets/equipment_history_dialog.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 = {}; @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 _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 _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.model?.vendor?.name ?? '', // Vendor 이름 e.equipment.serialNumber ?? '', // 시리얼 번호 (메인 필드) e.equipment.model?.name ?? '', // Model 이름 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; } // 선택된 장비들의 요약 정보를 가져와서 출고 폼으로 전달 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) { 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 != EquipmentStatus.disposed) .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( '${equipment.model?.vendor?.name ?? '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(); } /// 편집 핸들러 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) { 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; 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), // 상태 필터 드롭다운 (캐시된 데이터 사용) 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); } }, ), ), ], ); } /// 액션바 Widget _buildActionBar(int selectedCount, int selectedInCount, int selectedOutCount, int selectedRentCount, int totalCount) { return StandardActionBar( leftActions: [ // 라우트별 액션 버튼 _buildRouteSpecificActions(selectedInCount, selectedOutCount, selectedRentCount), ], 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 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) { // 입고 완료 후 데이터 새로고침 (중복 방지) _controller.refresh(); } }, variant: ShadcnButtonVariant.primary, textColor: Colors.white, icon: const Icon(Icons.add, size: 16), ), ], ); case Routes.equipmentOutList: return Row( 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), ), const SizedBox(width: 8), 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 Row( 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), ), const SizedBox(width: 8), 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 Row( 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), ), 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; // 기본 컬럼들 (리스트 API에서 제공하는 데이터만) totalWidth += 40; // 체크박스 totalWidth += 50; // 번호 totalWidth += 120; // 제조사 totalWidth += 120; // 장비번호 totalWidth += 120; // 모델명 totalWidth += 50; // 수량 totalWidth += 70; // 상태 totalWidth += 80; // 입출고일 totalWidth += 90; // 관리 // 상세 컬럼들 (조건부) if (_showDetailedColumns) { totalWidth += 120; // 시리얼번호 } // 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); } } /// 유연한 테이블 빌더 - Virtual Scrolling 적용 Widget _buildFlexibleTable(List pagedEquipments, {required bool useExpanded}) { final hasOutOrRent = pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent ); // 헤더를 별도로 빌드 Widget header = 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( ShadCheckbox( value: _isAllSelected(), onChanged: (bool? value) => _onSelectAll(value), ), 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: 3, useExpanded: useExpanded, minWidth: 120), // 상세 정보 (조건부) - 바코드로 변경 if (_showDetailedColumns) ...[ _buildHeaderCell('바코드', flex: 3, useExpanded: useExpanded, minWidth: 120), ], // 수량 _buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 50), // 재고 상태 _buildHeaderCell('재고', flex: 2, useExpanded: useExpanded, minWidth: 80), // 상태 _buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70), // 입출고일 _buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 80), // 관리 _buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90), ], ), ); // 빈 상태 처리 if (pagedEquipments.isEmpty) { return Column( children: [ header, Expanded( child: Center( child: Text( '데이터가 없습니다', style: ShadcnTheme.bodyMedium, ), ), ), ], ); } // Virtual Scrolling을 위한 CustomScrollView 사용 return Column( children: [ header, // 헤더는 고정 Expanded( child: ListView.builder( controller: ScrollController(), itemCount: pagedEquipments.length, itemBuilder: (context, index) { final UnifiedEquipment equipment = pagedEquipments[index]; return Container( padding: const EdgeInsets.symmetric( horizontal: ShadcnTheme.spacing4, vertical: 4, ), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: Colors.black), ), ), child: Row( children: [ // 체크박스 _buildDataCell( ShadCheckbox( 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.model?.vendor?.name ?? 'N/A', equipment.equipment.model?.vendor?.name ?? 'N/A', ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), // 장비번호 _buildDataCell( _buildTextWithTooltip( equipment.equipment.serialNumber ?? '', equipment.equipment.serialNumber ?? '', ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), // 모델명 _buildDataCell( _buildTextWithTooltip( equipment.equipment.model?.name ?? '-', equipment.equipment.model?.name ?? '-', ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), // 상세 정보 (조건부) - 바코드로 변경 if (_showDetailedColumns) ...[ _buildDataCell( _buildTextWithTooltip( equipment.equipment.barcode ?? '-', equipment.equipment.barcode ?? '-', ), flex: 3, useExpanded: useExpanded, minWidth: 120, ), ], // 수량 (백엔드에서 관리하지 않으므로 고정값) _buildDataCell( Text( '1', style: ShadcnTheme.bodySmall, ), flex: 1, useExpanded: useExpanded, minWidth: 50, ), // 재고 상태 _buildDataCell( _buildInventoryStatus(equipment), flex: 2, useExpanded: useExpanded, minWidth: 80, ), // 상태 _buildDataCell( _buildStatusBadge(equipment.status), flex: 2, useExpanded: useExpanded, minWidth: 70, ), // 입출고일 _buildDataCell( _buildCreatedDateWidget(equipment), flex: 2, useExpanded: useExpanded, minWidth: 80, ), // 관리 _buildDataCell( _buildActionButtons(equipment.equipment.id ?? 0), flex: 2, useExpanded: useExpanded, minWidth: 90, ), ], ), ); }, ), ), ], ); } /// 데이터 테이블 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 _buildInventoryStatus(UnifiedEquipment equipment) { // 백엔드 Equipment_History 기반으로 단순 상태만 표시 Widget stockInfo; if (equipment.status == EquipmentStatus.in_) { // 입고 상태: 재고 있음 stockInfo = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle, color: Colors.green, size: 16), const SizedBox(width: 4), Text( '보유중', style: ShadcnTheme.bodySmall.copyWith(color: Colors.green[700]), ), ], ); } else if (equipment.status == EquipmentStatus.out) { // 출고 상태: 재고 없음 stockInfo = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.warning, color: Colors.orange, size: 16), const SizedBox(width: 4), Text( '출고됨', style: ShadcnTheme.bodySmall.copyWith(color: Colors.orange[700]), ), ], ); } else if (equipment.status == EquipmentStatus.rent) { // 대여 상태 stockInfo = Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.schedule, color: Colors.blue, size: 16), const SizedBox(width: 4), Text( '대여중', style: ShadcnTheme.bodySmall.copyWith(color: Colors.blue[700]), ), ], ); } else { // 기타 상태 stockInfo = Text( '-', style: ShadcnTheme.bodySmall, ); } return stockInfo; } /// 상태 배지 빌더 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 _buildCreatedDateWidget(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.model?.vendor?.name ?? 'N/A'} ${equipment.equipment.serialNumber}', // Vendor + Equipment Number ); if (result == true) { _controller.loadData(isRefresh: true); } } // 편집 핸들러 (액션 버튼에서 호출) - 장비 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); } }); } // 사용하지 않는 카테고리 관련 함수들 제거됨 (리스트 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; } // 사용하지 않는 현재위치, 점검일 관련 함수들 제거됨 (리스트 API에서 제공하지 않음) }