Files
superport/lib/screens/equipment/equipment_list.dart
2025-07-02 17:45:44 +09:00

697 lines
28 KiB
Dart

import 'package:flutter/material.dart';
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
import 'package:superport/screens/equipment/widgets/equipment_table.dart';
import 'package:superport/utils/equipment_display_helper.dart';
import 'package:superport/services/mock_data_service.dart';
import 'package:superport/screens/common/theme_tailwind.dart';
import 'package:superport/screens/common/main_layout.dart';
import 'package:superport/utils/constants.dart';
import 'package:superport/models/equipment_unified_model.dart';
import 'package:superport/screens/common/widgets/pagination.dart';
// 장비 목록 화면 (UI만 담당, 상태/로직/헬퍼/위젯 분리)
class EquipmentListScreen extends StatefulWidget {
final String currentRoute;
const EquipmentListScreen({super.key, this.currentRoute = Routes.equipment});
@override
State<EquipmentListScreen> createState() => _EquipmentListScreenState();
}
class _EquipmentListScreenState extends State<EquipmentListScreen> {
late final EquipmentListController _controller;
bool _showDetailedColumns = true;
final ScrollController _horizontalScrollController = ScrollController();
final ScrollController _verticalScrollController = ScrollController();
int _currentPage = 1;
final int _pageSize = 10;
String _searchKeyword = '';
String _appliedSearchKeyword = '';
@override
void initState() {
super.initState();
_controller = EquipmentListController(dataService: MockDataService());
_controller.loadData();
WidgetsBinding.instance.addPostFrameCallback((_) {
_adjustColumnsForScreenSize();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_setDefaultFilterByRoute();
}
@override
void didUpdateWidget(EquipmentListScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentRoute != widget.currentRoute) {
_setDefaultFilterByRoute();
}
}
@override
void dispose() {
_horizontalScrollController.dispose();
_verticalScrollController.dispose();
super.dispose();
}
// 라우트에 따라 기본 필터 설정
void _setDefaultFilterByRoute() {
String? newFilter;
if (widget.currentRoute == Routes.equipmentInList) {
newFilter = EquipmentStatus.in_;
} else if (widget.currentRoute == Routes.equipmentOutList) {
newFilter = EquipmentStatus.out;
} else if (widget.currentRoute == Routes.equipmentRentList) {
newFilter = EquipmentStatus.rent;
} else if (widget.currentRoute == Routes.equipment) {
newFilter = null;
}
if ((newFilter != _controller.selectedStatusFilter) ||
widget.currentRoute != Routes.equipment) {
setState(() {
_controller.selectedStatusFilter = newFilter;
_controller.loadData();
});
}
}
// 화면 크기에 따라 컬럼 표시 조정
void _adjustColumnsForScreenSize() {
final width = MediaQuery.of(context).size.width;
setState(() {
_showDetailedColumns = width > 900;
});
}
// 상태 필터 변경
void _onStatusFilterChanged(String? status) {
setState(() {
_controller.changeStatusFilter(status);
});
}
// 장비 선택/해제
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
setState(() {
_controller.selectEquipment(id, status, isSelected);
});
}
// 출고 처리 버튼 핸들러
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),
),
);
}).toList(),
],
),
),
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('폐기'),
),
],
),
);
}
// 카테고리 축약 표기 함수 (예: 컴... > 태... > 안드로...)
String _shortenCategory(String category) {
if (category.length <= 2) return category;
return category.substring(0, 2) + '...';
}
// 카테고리 툴팁 위젯 (UI만 담당, 축약 표기 적용)
Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) {
final fullCategory = EquipmentDisplayHelper.formatCategory(
equipment.equipment.category,
equipment.equipment.subCategory,
equipment.equipment.subSubCategory,
);
// 축약 표기 적용
final shortCategory = [
_shortenCategory(equipment.equipment.category),
_shortenCategory(equipment.equipment.subCategory),
_shortenCategory(equipment.equipment.subSubCategory),
].join(' > ');
return Tooltip(message: fullCategory, child: Text(shortCategory));
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32;
String screenTitle = '장비 목록';
if (widget.currentRoute == Routes.equipmentInList) {
screenTitle = '입고된 장비';
} else if (widget.currentRoute == Routes.equipmentOutList) {
screenTitle = '출고된 장비';
} else if (widget.currentRoute == Routes.equipmentRentList) {
screenTitle = '대여된 장비';
}
final int totalCount = _controller.equipments.length;
final List<UnifiedEquipment> filteredEquipments =
_appliedSearchKeyword.isEmpty
? _controller.equipments
: _controller.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();
final int filteredCount = filteredEquipments.length;
final int startIndex = (_currentPage - 1) * _pageSize;
final int endIndex =
(startIndex + _pageSize) > filteredCount
? filteredCount
: (startIndex + _pageSize);
final pagedEquipments = filteredEquipments.sublist(startIndex, endIndex);
// 선택된 장비 개수
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 MainLayout(
title: screenTitle,
currentRoute: widget.currentRoute,
actions: [
IconButton(
icon: Icon(
_showDetailedColumns ? Icons.view_column : Icons.view_compact,
color: Colors.grey,
),
tooltip: _showDetailedColumns ? '간소화된 보기' : '상세 보기',
onPressed: () {
setState(() {
_showDetailedColumns = !_showDetailedColumns;
});
},
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {
_controller.loadData();
_currentPage = 1;
});
},
color: Colors.grey,
),
],
child: Container(
width: maxContentWidth,
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
screenTitle,
style: AppThemeTailwind.headingStyle,
),
),
if (selectedCount > 0)
Container(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$selectedCount개 선택됨',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
if (widget.currentRoute == Routes.equipmentInList)
Row(
children: [
ElevatedButton.icon(
onPressed:
selectedInCount > 0 ? _handleOutEquipment : null,
icon: const Icon(
Icons.exit_to_app,
color: Colors.white,
),
label: const Text(
'출고',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
disabledBackgroundColor: Colors.blue.withOpacity(0.5),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () async {
final result = await Navigator.pushNamed(
context,
Routes.equipmentInAdd,
);
if (result == true) {
setState(() {
_controller.loadData();
_currentPage = 1;
});
}
},
icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
'입고',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 16),
SizedBox(
width: 220,
child: Row(
children: [
Expanded(
child: TextField(
decoration: const InputDecoration(
hintText: '장비 검색',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
isDense: true,
contentPadding: EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
),
onChanged: (value) {
setState(() {
_searchKeyword = value;
});
},
onSubmitted: (value) {
setState(() {
_appliedSearchKeyword = value;
_currentPage = 1;
});
},
),
),
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.arrow_forward),
tooltip: '검색',
onPressed: () {
setState(() {
_appliedSearchKeyword = _searchKeyword;
_currentPage = 1;
});
},
),
],
),
),
],
),
// 출고 목록 화면일 때 버튼들
if (widget.currentRoute == Routes.equipmentOutList)
Row(
children: [
ElevatedButton.icon(
onPressed:
selectedOutCount > 0
? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('재입고 기능은 준비 중입니다.'),
),
);
}
: null,
icon: const Icon(
Icons.assignment_return,
color: Colors.white,
),
label: const Text(
'재입고',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
disabledBackgroundColor: Colors.green.withOpacity(
0.5,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed:
selectedOutCount > 0
? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('수리 요청 기능은 준비 중입니다.'),
),
);
}
: null,
icon: const Icon(Icons.build, color: Colors.white),
label: const Text(
'수리 요청',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
disabledBackgroundColor: Colors.orange.withOpacity(
0.5,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
],
),
// 대여 목록 화면일 때 버튼들
if (widget.currentRoute == Routes.equipmentRentList)
Row(
children: [
ElevatedButton.icon(
onPressed:
selectedRentCount > 0
? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('대여 반납 기능은 준비 중입니다.'),
),
);
}
: null,
icon: const Icon(
Icons.keyboard_return,
color: Colors.white,
),
label: const Text(
'반납',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
disabledBackgroundColor: Colors.green.withOpacity(
0.5,
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed:
selectedRentCount > 0
? () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('대여 연장 기능은 준비 중입니다.'),
),
);
}
: null,
icon: const Icon(Icons.date_range, color: Colors.white),
label: const Text(
'연장',
style: TextStyle(color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
disabledBackgroundColor: Colors.blue.withOpacity(0.5),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
textStyle: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
const SizedBox(height: 8),
Expanded(
child:
pagedEquipments.isEmpty
? const Center(child: Text('장비 정보가 없습니다.'))
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: maxContentWidth,
),
child: EquipmentTable(
equipments: pagedEquipments,
selectedEquipmentIds:
_controller.selectedEquipmentIds,
showDetailedColumns: _showDetailedColumns,
onEquipmentSelected: _onEquipmentSelected,
getOutEquipmentInfo:
_controller.getOutEquipmentInfo,
buildCategoryWithTooltip: _buildCategoryWithTooltip,
// 수정 버튼 동작: 입고 폼(수정 모드)로 이동
onEdit: (id, status) async {
if (status == EquipmentStatus.in_) {
final result = await Navigator.pushNamed(
context,
Routes.equipmentInEdit,
arguments: id,
);
if (result == true) {
setState(() {
_controller.loadData();
});
}
} else {
// 출고/대여 등은 별도 폼으로 이동 필요시 구현
}
},
// 삭제 버튼 동작: 삭제 다이얼로그 및 삭제 처리
onDelete: (id, status) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('삭제 확인'),
content: const Text('이 장비 정보를 삭제하시겠습니까?'),
actions: [
TextButton(
onPressed:
() => Navigator.pop(context),
child: const Text('취소'),
),
TextButton(
onPressed: () {
setState(() {
// 입고/출고 상태에 따라 삭제 처리
if (status ==
EquipmentStatus.in_) {
MockDataService()
.deleteEquipmentIn(id);
} else if (status ==
EquipmentStatus.out) {
MockDataService()
.deleteEquipmentOut(id);
}
_controller.loadData();
});
Navigator.pop(context);
},
child: const Text('삭제'),
),
],
),
);
},
getSelectedInStockCount:
_controller.getSelectedInStockCount,
),
),
),
),
if (totalCount > _pageSize)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Pagination(
totalCount: filteredCount,
currentPage: _currentPage,
pageSize: _pageSize,
onPageChanged: (page) {
setState(() {
_currentPage = page;
});
},
),
),
],
),
),
);
}
}