- EquipmentHistoryDto 모델 확장 (상세 정보 추가) - 장비 이력 화면 UI/UX 개선 - 장비 입고 폼 검증 로직 강화 - 테스트 이력 화면 추가 - API 응답 처리 개선 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1155 lines
45 KiB
Dart
1155 lines
45 KiB
Dart
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/unified_search_bar.dart';
|
|
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
|
import 'package:superport/screens/common/widgets/standard_data_table.dart' as std_table;
|
|
import 'package:superport/screens/common/widgets/standard_states.dart';
|
|
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
|
import 'package:superport/services/mock_data_service.dart';
|
|
import 'package:superport/models/equipment_unified_model.dart';
|
|
import 'package:superport/utils/constants.dart';
|
|
import 'package:superport/utils/equipment_display_helper.dart';
|
|
|
|
/// shadcn/ui 스타일로 재설계된 장비 관리 화면
|
|
class EquipmentListRedesign extends StatefulWidget {
|
|
final String currentRoute;
|
|
|
|
const EquipmentListRedesign({Key? key, this.currentRoute = Routes.equipment})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<EquipmentListRedesign> createState() => _EquipmentListRedesignState();
|
|
}
|
|
|
|
class _EquipmentListRedesignState extends State<EquipmentListRedesign> {
|
|
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 = '';
|
|
int _currentPage = 1;
|
|
final int _pageSize = 10;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = EquipmentListController(dataService: MockDataService());
|
|
_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<void> _loadData({bool isRefresh = false}) async {
|
|
await _controller.loadData(isRefresh: isRefresh);
|
|
}
|
|
|
|
/// 상태 필터 변경
|
|
Future<void> _onStatusFilterChanged(String status) async {
|
|
setState(() {
|
|
_selectedStatus = status;
|
|
// 상태 필터를 EquipmentStatus 상수로 변환
|
|
if (status == 'all') {
|
|
_controller.selectedStatusFilter = null;
|
|
} else if (status == 'in') {
|
|
_controller.selectedStatusFilter = EquipmentStatus.in_;
|
|
} else if (status == 'out') {
|
|
_controller.selectedStatusFilter = EquipmentStatus.out;
|
|
} else if (status == 'rent') {
|
|
_controller.selectedStatusFilter = EquipmentStatus.rent;
|
|
}
|
|
_currentPage = 1;
|
|
});
|
|
await _controller.changeStatusFilter(_controller.selectedStatusFilter);
|
|
}
|
|
|
|
/// 검색 실행
|
|
void _onSearch() async {
|
|
setState(() {
|
|
_appliedSearchKeyword = _searchController.text;
|
|
_currentPage = 1;
|
|
});
|
|
await _controller.updateSearchKeyword(_searchController.text);
|
|
}
|
|
|
|
/// 장비 선택/해제
|
|
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
|
setState(() {
|
|
_controller.selectEquipment(id, status, isSelected);
|
|
});
|
|
}
|
|
|
|
/// 전체 선택/해제
|
|
void _onSelectAll(bool? value) {
|
|
setState(() {
|
|
final equipments = _getFilteredEquipments();
|
|
for (final equipment in equipments) {
|
|
_controller.selectEquipment(equipment.id, equipment.status, value);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 전체 선택 상태 확인
|
|
bool _isAllSelected() {
|
|
final equipments = _getFilteredEquipments();
|
|
if (equipments.isEmpty) return false;
|
|
return equipments.every((e) =>
|
|
_controller.selectedEquipmentIds.contains('${e.id}:${e.status}'));
|
|
}
|
|
|
|
|
|
/// 필터링된 장비 목록 반환
|
|
List<UnifiedEquipment> _getFilteredEquipments() {
|
|
var equipments = _controller.equipments;
|
|
print('DEBUG: Total equipments from controller: ${equipments.length}'); // 디버그 정보
|
|
|
|
// 검색 키워드 적용 (확장된 검색 필드)
|
|
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();
|
|
}
|
|
|
|
print('DEBUG: Filtered equipments count: ${equipments.length}'); // 디버그 정보
|
|
print('DEBUG: Selected status filter: $_selectedStatus'); // 디버그 정보
|
|
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();
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 대여 처리 버튼 핸들러
|
|
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() {
|
|
if (_controller.getSelectedInStockCount() == 0) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('폐기할 장비를 선택해주세요.')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary();
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('폐기 확인'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('선택한 ${selectedEquipmentsSummary.length}개 장비를 폐기하시겠습니까?'),
|
|
const SizedBox(height: 16),
|
|
const Text('폐기할 장비 목록:', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
...selectedEquipmentsSummary.map((equipmentData) {
|
|
final equipment = equipmentData['equipment'] as Equipment;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: Text(
|
|
'${equipment.manufacturer} ${equipment.name} (${equipment.quantity}개)',
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('취소'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('폐기 기능은 준비 중입니다.')),
|
|
);
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text('폐기'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 편집 핸들러
|
|
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();
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 삭제 핸들러
|
|
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를 통한 삭제 처리
|
|
final success = await _controller.deleteEquipment(equipment);
|
|
|
|
// 로딩 다이얼로그 닫기
|
|
if (mounted) Navigator.pop(context);
|
|
|
|
if (success) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
|
);
|
|
}
|
|
} else {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_controller.error ?? '삭제 중 오류가 발생했습니다.'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
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 Navigator.pushNamed(
|
|
context,
|
|
Routes.equipmentHistory,
|
|
arguments: {
|
|
'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<EquipmentListController>.value(
|
|
value: _controller,
|
|
child: Consumer<EquipmentListController>(
|
|
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);
|
|
|
|
return Container(
|
|
color: ShadcnTheme.background,
|
|
child: Column(
|
|
children: [
|
|
// 필터 및 액션 바
|
|
_buildFilterBar(selectedCount, selectedInCount, selectedOutCount, selectedRentCount),
|
|
|
|
// 장비 테이블
|
|
Expanded(
|
|
child: controller.isLoading
|
|
? _buildLoadingState()
|
|
: controller.error != null
|
|
? _buildErrorState()
|
|
: _buildEquipmentTable(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 필터 바
|
|
Widget _buildFilterBar(int selectedCount, int selectedInCount, int selectedOutCount, int selectedRentCount) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
children: [
|
|
// 검색 및 필터 섹션
|
|
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<String>(
|
|
value: _selectedStatus,
|
|
onChanged: (value) => _onStatusFilterChanged(value!),
|
|
style: TextStyle(fontSize: 14, color: ShadcnTheme.foreground),
|
|
icon: const Icon(Icons.arrow_drop_down, size: 20),
|
|
items: const [
|
|
DropdownMenuItem(value: 'all', child: Text('전체')),
|
|
DropdownMenuItem(value: 'in', child: Text('입고')),
|
|
DropdownMenuItem(value: 'out', child: Text('출고')),
|
|
DropdownMenuItem(value: 'rent', child: Text('대여')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 액션 버튼들 및 상태 표시
|
|
Row(
|
|
children: [
|
|
// 라우트별 액션 버튼
|
|
_buildRouteSpecificActions(selectedInCount, selectedOutCount, selectedRentCount),
|
|
|
|
const Spacer(),
|
|
|
|
// 선택 및 총 개수 표시
|
|
if (selectedCount > 0)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 8,
|
|
horizontal: 16,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusSm),
|
|
),
|
|
child: Text(
|
|
'$selectedCount개 선택됨',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
if (selectedCount > 0) const SizedBox(width: 12),
|
|
Text(
|
|
'총 ${_getFilteredEquipments().length}개',
|
|
style: ShadcnTheme.bodyMuted,
|
|
),
|
|
const SizedBox(width: 12),
|
|
// 새로고침 버튼
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: () {
|
|
setState(() {
|
|
_controller.loadData();
|
|
_currentPage = 1;
|
|
});
|
|
},
|
|
),
|
|
// 뷰 모드 전환 버튼
|
|
IconButton(
|
|
icon: Icon(_showDetailedColumns ? Icons.view_column : Icons.view_compact),
|
|
tooltip: _showDetailedColumns ? '간소화된 보기' : '상세 보기',
|
|
onPressed: () {
|
|
setState(() {
|
|
_showDetailedColumns = !_showDetailedColumns;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 라우트별 액션 버튼
|
|
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();
|
|
_currentPage = 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();
|
|
_currentPage = 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),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 로딩 상태
|
|
Widget _buildLoadingState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(color: ShadcnTheme.primary),
|
|
const SizedBox(height: 16),
|
|
Text('데이터를 불러오는 중...', style: ShadcnTheme.bodyMuted),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 에러 상태 위젯
|
|
Widget _buildErrorState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 48, color: ShadcnTheme.destructive),
|
|
const SizedBox(height: 16),
|
|
Text('데이터를 불러오는 중 오류가 발생했습니다.', style: ShadcnTheme.bodyMuted),
|
|
const SizedBox(height: 8),
|
|
Text(_controller.error ?? '', style: ShadcnTheme.bodySmall),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () => _controller.loadData(isRefresh: true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ShadcnTheme.primary,
|
|
),
|
|
child: const Text('다시 시도'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 테이블 너비 계산
|
|
double _calculateTableWidth(List<UnifiedEquipment> 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; // 바코드
|
|
|
|
// 출고 정보 (조건부)
|
|
if (pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent)) {
|
|
totalWidth += 120; // 회사
|
|
totalWidth += 80; // 담당자
|
|
}
|
|
}
|
|
|
|
return totalWidth;
|
|
}
|
|
|
|
/// 장비 테이블
|
|
Widget _buildEquipmentTable() {
|
|
final filteredEquipments = _getFilteredEquipments();
|
|
final totalCount = filteredEquipments.length;
|
|
final int startIndex = (_currentPage - 1) * _pageSize;
|
|
final int endIndex =
|
|
(startIndex + _pageSize) > totalCount
|
|
? totalCount
|
|
: (startIndex + _pageSize);
|
|
final List<UnifiedEquipment> pagedEquipments = filteredEquipments.sublist(
|
|
startIndex,
|
|
endIndex,
|
|
);
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
children: [
|
|
// 테이블 컨테이너 (가로 스크롤 지원)
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
controller: _horizontalScrollController,
|
|
child: Container(
|
|
constraints: BoxConstraints(
|
|
minWidth: MediaQuery.of(context).size.width - 48, // padding 고려
|
|
),
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.black),
|
|
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
|
),
|
|
child:
|
|
pagedEquipments.isEmpty
|
|
? Container(
|
|
padding: const EdgeInsets.all(64),
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.inventory_2_outlined,
|
|
size: 48,
|
|
color: ShadcnTheme.mutedForeground,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text('장비가 없습니다', style: ShadcnTheme.bodyMuted),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: SizedBox(
|
|
width: _calculateTableWidth(pagedEquipments),
|
|
child: 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: [
|
|
// 체크박스
|
|
SizedBox(
|
|
width: 40,
|
|
child: Checkbox(
|
|
value: _isAllSelected(),
|
|
onChanged: _onSelectAll,
|
|
),
|
|
),
|
|
// 번호
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text('번호', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 제조사
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text('제조사', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 장비명
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text('장비명', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 카테고리
|
|
SizedBox(
|
|
width: 100,
|
|
child: Text('카테고리', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 상세 정보 (조건부)
|
|
if (_showDetailedColumns) ...[
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text('시리얼번호', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text('바코드', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
],
|
|
// 수량
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text('수량', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 상태
|
|
SizedBox(
|
|
width: 70,
|
|
child: Text('상태', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 날짜
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text('날짜', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
// 출고 정보 (조건부 - 테이블에 출고/대여 항목이 있을 때만)
|
|
if (_showDetailedColumns && pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent)) ...[
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text('회사', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text('담당자', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
],
|
|
// 관리
|
|
SizedBox(
|
|
width: 90,
|
|
child: Text('관리', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// 테이블 데이터
|
|
...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: [
|
|
// 체크박스
|
|
SizedBox(
|
|
width: 40,
|
|
child: Checkbox(
|
|
value: _controller.selectedEquipmentIds.contains('${equipment.id}:${equipment.status}'),
|
|
onChanged: (value) => _onEquipmentSelected(equipment.id, equipment.status, value),
|
|
),
|
|
),
|
|
// 번호
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
'${startIndex + index + 1}',
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
),
|
|
// 제조사
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
equipment.equipment.manufacturer,
|
|
style: ShadcnTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
// 장비명
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
equipment.equipment.name,
|
|
style: ShadcnTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
// 카테고리
|
|
SizedBox(
|
|
width: 100,
|
|
child: _buildCategoryWithTooltip(equipment),
|
|
),
|
|
// 상세 정보 (조건부)
|
|
if (_showDetailedColumns) ...[
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
equipment.equipment.serialNumber ?? '-',
|
|
style: ShadcnTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
equipment.equipment.barcode ?? '-',
|
|
style: ShadcnTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
// 수량
|
|
SizedBox(
|
|
width: 50,
|
|
child: Text(
|
|
'${equipment.equipment.quantity}',
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
),
|
|
// 상태
|
|
SizedBox(
|
|
width: 70,
|
|
child: ShadcnBadge(
|
|
text: _getStatusDisplayText(
|
|
equipment.status,
|
|
),
|
|
variant: _getStatusBadgeVariant(
|
|
equipment.status,
|
|
),
|
|
size: ShadcnBadgeSize.small,
|
|
),
|
|
),
|
|
// 날짜
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
equipment.date.toString().substring(0, 10),
|
|
style: ShadcnTheme.bodySmall,
|
|
),
|
|
),
|
|
// 출고 정보 (조건부)
|
|
if (_showDetailedColumns && pagedEquipments.any((e) => e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent)) ...[
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(
|
|
equipment.status == EquipmentStatus.out || equipment.status == EquipmentStatus.rent
|
|
? _controller.getOutEquipmentInfo(equipment.id!, 'company')
|
|
: '-',
|
|
style: ShadcnTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
equipment.status == EquipmentStatus.out || equipment.status == EquipmentStatus.rent
|
|
? _controller.getOutEquipmentInfo(equipment.id!, 'manager')
|
|
: '-',
|
|
style: ShadcnTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
// 관리 버튼
|
|
SizedBox(
|
|
width: 90,
|
|
child: 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: () => _handleHistory(equipment),
|
|
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: () => _handleEdit(equipment),
|
|
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: () => _handleDelete(equipment),
|
|
tooltip: '삭제',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// 페이지네이션 컴포넌트
|
|
if (totalCount > _pageSize)
|
|
Pagination(
|
|
totalCount: totalCount,
|
|
currentPage: _currentPage,
|
|
pageSize: _pageSize,
|
|
onPageChanged: (page) {
|
|
setState(() {
|
|
_currentPage = page;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 상태 표시 텍스트 반환
|
|
String _getStatusDisplayText(String status) {
|
|
switch (status) {
|
|
case 'I': // EquipmentStatus.in_
|
|
return '입고';
|
|
case 'O': // EquipmentStatus.out
|
|
return '출고';
|
|
case 'T': // EquipmentStatus.rent
|
|
return '대여';
|
|
default:
|
|
return '알수없음';
|
|
}
|
|
}
|
|
|
|
/// 상태에 따른 배지 변형 반환
|
|
ShadcnBadgeVariant _getStatusBadgeVariant(String status) {
|
|
switch (status) {
|
|
case 'I': // EquipmentStatus.in_
|
|
return ShadcnBadgeVariant.success;
|
|
case 'O': // EquipmentStatus.out
|
|
return ShadcnBadgeVariant.destructive;
|
|
case 'T': // EquipmentStatus.rent
|
|
return ShadcnBadgeVariant.warning;
|
|
default:
|
|
return ShadcnBadgeVariant.secondary;
|
|
}
|
|
}
|
|
|
|
/// 카테고리 축약 표기 함수
|
|
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<String> 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,
|
|
),
|
|
);
|
|
}
|
|
}
|