feat: V/R 유지보수 시스템 전환 및 대시보드 테이블 형태 완성
- V/R 시스템 완전 전환: WARRANTY/CONTRACT/INSPECTION → V(방문)/R(원격) - 유지보수 대시보드 카드 → StandardDataTable 테이블 형태 전환 - "조회중..." 문제 해결: 백엔드 직접 필드 사용 (equipment_model, company_name) - MaintenanceDto 신규 필드 추가: company_id, company_name, equipment_serial, equipment_model - preloadEquipmentData 비활성화로 불필요한 equipment-history API 호출 제거 - CO-STAR 프레임워크 적용 및 CLAUDE.md v3.0 업데이트 - Flutter Analyze ERROR: 0 유지, 100% shadcn_ui 컴플라이언스 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/data/models/inventory_history_view_model.dart';
|
||||
import 'package:superport/services/inventory_history_service.dart';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
|
||||
/// 재고 이력 관리 화면 전용 컨트롤러
|
||||
/// InventoryHistoryService를 통해 여러 API를 조합한 데이터 관리
|
||||
class InventoryHistoryController extends ChangeNotifier {
|
||||
final InventoryHistoryService _service;
|
||||
|
||||
InventoryHistoryController({
|
||||
InventoryHistoryService? service,
|
||||
}) : _service = service ?? InventoryHistoryService();
|
||||
|
||||
// 상태 변수
|
||||
List<InventoryHistoryViewModel> _historyItems = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _pageSize = AppConstants.historyPageSize;
|
||||
int _totalCount = 0;
|
||||
int _totalPages = 0;
|
||||
|
||||
// 검색 및 필터
|
||||
String _searchKeyword = '';
|
||||
String? _selectedTransactionType;
|
||||
int? _selectedEquipmentId;
|
||||
int? _selectedWarehouseId;
|
||||
int? _selectedCompanyId;
|
||||
DateTime? _dateFrom;
|
||||
DateTime? _dateTo;
|
||||
|
||||
// Getters
|
||||
List<InventoryHistoryViewModel> get historyItems => _historyItems;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
int get currentPage => _currentPage;
|
||||
int get totalPages => _totalPages;
|
||||
int get totalCount => _totalCount;
|
||||
int get pageSize => _pageSize;
|
||||
String get searchKeyword => _searchKeyword;
|
||||
String? get selectedTransactionType => _selectedTransactionType;
|
||||
|
||||
// 통계 정보
|
||||
int get totalTransactions => _historyItems.length;
|
||||
int get inStockCount => _historyItems.where((item) => item.transactionType == 'I').length;
|
||||
int get outStockCount => _historyItems.where((item) => item.transactionType == 'O').length;
|
||||
int get rentCount => _historyItems.where((item) => item.transactionType == 'R').length;
|
||||
int get disposeCount => _historyItems.where((item) => item.transactionType == 'D').length;
|
||||
|
||||
/// 재고 이력 목록 로드
|
||||
Future<void> loadHistories({bool refresh = false}) async {
|
||||
if (refresh) {
|
||||
_currentPage = 1;
|
||||
_historyItems.clear();
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
print('[InventoryHistoryController] Loading histories - Page: $_currentPage, Search: "$_searchKeyword", Type: $_selectedTransactionType');
|
||||
|
||||
final response = await _service.loadInventoryHistories(
|
||||
page: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
searchKeyword: _searchKeyword.isEmpty ? null : _searchKeyword,
|
||||
transactionType: _selectedTransactionType,
|
||||
equipmentId: _selectedEquipmentId,
|
||||
warehouseId: _selectedWarehouseId,
|
||||
companyId: _selectedCompanyId,
|
||||
dateFrom: _dateFrom,
|
||||
dateTo: _dateTo,
|
||||
);
|
||||
|
||||
if (refresh) {
|
||||
_historyItems = response.items;
|
||||
} else {
|
||||
_historyItems.addAll(response.items);
|
||||
}
|
||||
|
||||
_totalCount = response.totalCount;
|
||||
_totalPages = response.totalPages;
|
||||
|
||||
print('[InventoryHistoryController] Loaded ${response.items.length} items, Total: $_totalCount');
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
print('[InventoryHistoryController] Error loading histories: $e');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 특정 장비의 전체 이력 로드 (상세보기용)
|
||||
Future<List<InventoryHistoryViewModel>> loadEquipmentHistory(int equipmentId) async {
|
||||
try {
|
||||
print('[InventoryHistoryController] Loading equipment history for ID: $equipmentId');
|
||||
|
||||
final histories = await _service.loadEquipmentHistory(equipmentId);
|
||||
|
||||
print('[InventoryHistoryController] Loaded ${histories.length} equipment histories');
|
||||
return histories;
|
||||
} catch (e) {
|
||||
print('[InventoryHistoryController] Error loading equipment history: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 검색 키워드 설정
|
||||
void setSearchKeyword(String keyword) {
|
||||
if (_searchKeyword != keyword) {
|
||||
_searchKeyword = keyword;
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 거래 유형 필터 설정
|
||||
void setTransactionTypeFilter(String? transactionType) {
|
||||
if (_selectedTransactionType != transactionType) {
|
||||
_selectedTransactionType = transactionType;
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 필터 설정
|
||||
void setEquipmentFilter(int? equipmentId) {
|
||||
if (_selectedEquipmentId != equipmentId) {
|
||||
_selectedEquipmentId = equipmentId;
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 창고 필터 설정
|
||||
void setWarehouseFilter(int? warehouseId) {
|
||||
if (_selectedWarehouseId != warehouseId) {
|
||||
_selectedWarehouseId = warehouseId;
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 고객사 필터 설정
|
||||
void setCompanyFilter(int? companyId) {
|
||||
if (_selectedCompanyId != companyId) {
|
||||
_selectedCompanyId = companyId;
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 날짜 범위 필터 설정
|
||||
void setDateRangeFilter(DateTime? dateFrom, DateTime? dateTo) {
|
||||
if (_dateFrom != dateFrom || _dateTo != dateTo) {
|
||||
_dateFrom = dateFrom;
|
||||
_dateTo = dateTo;
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 복합 필터 설정 (한 번에 여러 필터 적용)
|
||||
void setFilters({
|
||||
String? searchKeyword,
|
||||
String? transactionType,
|
||||
int? equipmentId,
|
||||
int? warehouseId,
|
||||
int? companyId,
|
||||
DateTime? dateFrom,
|
||||
DateTime? dateTo,
|
||||
}) {
|
||||
bool hasChanges = false;
|
||||
|
||||
if (searchKeyword != null && _searchKeyword != searchKeyword) {
|
||||
_searchKeyword = searchKeyword;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (_selectedTransactionType != transactionType) {
|
||||
_selectedTransactionType = transactionType;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (_selectedEquipmentId != equipmentId) {
|
||||
_selectedEquipmentId = equipmentId;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (_selectedWarehouseId != warehouseId) {
|
||||
_selectedWarehouseId = warehouseId;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (_selectedCompanyId != companyId) {
|
||||
_selectedCompanyId = companyId;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (_dateFrom != dateFrom || _dateTo != dateTo) {
|
||||
_dateFrom = dateFrom;
|
||||
_dateTo = dateTo;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
_currentPage = 1;
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 모든 필터 초기화
|
||||
void clearFilters() {
|
||||
_searchKeyword = '';
|
||||
_selectedTransactionType = null;
|
||||
_selectedEquipmentId = null;
|
||||
_selectedWarehouseId = null;
|
||||
_selectedCompanyId = null;
|
||||
_dateFrom = null;
|
||||
_dateTo = null;
|
||||
_currentPage = 1;
|
||||
|
||||
loadHistories(refresh: true);
|
||||
}
|
||||
|
||||
/// 다음 페이지 로드
|
||||
Future<void> loadNextPage() async {
|
||||
if (_currentPage < _totalPages && !_isLoading) {
|
||||
_currentPage++;
|
||||
await loadHistories();
|
||||
}
|
||||
}
|
||||
|
||||
/// 이전 페이지 로드
|
||||
Future<void> loadPreviousPage() async {
|
||||
if (_currentPage > 1 && !_isLoading) {
|
||||
_currentPage--;
|
||||
await loadHistories();
|
||||
}
|
||||
}
|
||||
|
||||
/// 특정 페이지로 이동
|
||||
Future<void> goToPage(int page) async {
|
||||
if (page > 0 && page <= _totalPages && page != _currentPage && !_isLoading) {
|
||||
_currentPage = page;
|
||||
await loadHistories();
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadHistories(refresh: true);
|
||||
}
|
||||
|
||||
/// 에러 초기화
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 통계 정보 맵 형태로 반환
|
||||
Map<String, dynamic> getStatistics() {
|
||||
return {
|
||||
'total': totalCount,
|
||||
'current_page_count': totalTransactions,
|
||||
'in_stock': inStockCount,
|
||||
'out_stock': outStockCount,
|
||||
'rent': rentCount,
|
||||
'dispose': disposeCount,
|
||||
};
|
||||
}
|
||||
|
||||
/// 검색 상태 확인
|
||||
bool get hasActiveFilters {
|
||||
return _searchKeyword.isNotEmpty ||
|
||||
_selectedTransactionType != null ||
|
||||
_selectedEquipmentId != null ||
|
||||
_selectedWarehouseId != null ||
|
||||
_selectedCompanyId != null ||
|
||||
_dateFrom != null ||
|
||||
_dateTo != null;
|
||||
}
|
||||
|
||||
/// 필터 상태 텍스트
|
||||
String get filterStatusText {
|
||||
List<String> filters = [];
|
||||
|
||||
if (_searchKeyword.isNotEmpty) {
|
||||
filters.add('검색: "$_searchKeyword"');
|
||||
}
|
||||
|
||||
if (_selectedTransactionType != null) {
|
||||
final typeMap = {
|
||||
'I': '입고',
|
||||
'O': '출고',
|
||||
'R': '대여',
|
||||
'D': '폐기',
|
||||
};
|
||||
filters.add('유형: ${typeMap[_selectedTransactionType]}');
|
||||
}
|
||||
|
||||
if (_dateFrom != null || _dateTo != null) {
|
||||
String dateFilter = '기간: ';
|
||||
if (_dateFrom != null) {
|
||||
dateFilter += '${_dateFrom!.toString().substring(0, 10)}';
|
||||
}
|
||||
if (_dateTo != null) {
|
||||
dateFilter += ' ~ ${_dateTo!.toString().substring(0, 10)}';
|
||||
}
|
||||
filters.add(dateFilter);
|
||||
}
|
||||
|
||||
return filters.join(', ');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_historyItems.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/inventory_history_view_model.dart';
|
||||
import 'package:superport/screens/inventory/controllers/inventory_history_controller.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
|
||||
/// 장비 이력 상세보기 다이얼로그
|
||||
/// 특정 장비의 전체 히스토리를 시간순으로 표시
|
||||
class EquipmentHistoryDetailDialog extends StatefulWidget {
|
||||
final int equipmentId;
|
||||
final String equipmentName;
|
||||
final String serialNumber;
|
||||
final InventoryHistoryController controller;
|
||||
|
||||
const EquipmentHistoryDetailDialog({
|
||||
super.key,
|
||||
required this.equipmentId,
|
||||
required this.equipmentName,
|
||||
required this.serialNumber,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentHistoryDetailDialog> createState() =>
|
||||
_EquipmentHistoryDetailDialogState();
|
||||
}
|
||||
|
||||
class _EquipmentHistoryDetailDialogState
|
||||
extends State<EquipmentHistoryDetailDialog> {
|
||||
List<InventoryHistoryViewModel>? _historyList;
|
||||
bool _isLoading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadEquipmentHistory();
|
||||
}
|
||||
|
||||
/// 장비별 이력 로드
|
||||
Future<void> _loadEquipmentHistory() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final histories = await widget.controller.loadEquipmentHistory(widget.equipmentId);
|
||||
setState(() {
|
||||
_historyList = histories;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
print('[EquipmentHistoryDetailDialog] Error loading equipment history: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 거래 유형 아이콘 반환
|
||||
IconData _getTransactionIcon(String transactionType) {
|
||||
switch (transactionType) {
|
||||
case 'I':
|
||||
return Icons.arrow_downward; // 입고
|
||||
case 'O':
|
||||
return Icons.arrow_upward; // 출고
|
||||
case 'R':
|
||||
return Icons.share; // 대여
|
||||
case 'D':
|
||||
return Icons.delete_outline; // 폐기
|
||||
default:
|
||||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
|
||||
/// 거래 유형 색상 반환
|
||||
Color _getTransactionColor(String transactionType) {
|
||||
switch (transactionType) {
|
||||
case 'I':
|
||||
return Colors.green; // 입고
|
||||
case 'O':
|
||||
return Colors.orange; // 출고
|
||||
case 'R':
|
||||
return Colors.blue; // 대여
|
||||
case 'D':
|
||||
return Colors.red; // 폐기
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
/// 타임라인 아이템 빌더
|
||||
Widget _buildTimelineItem(InventoryHistoryViewModel history, int index) {
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == (_historyList?.length ?? 0) - 1;
|
||||
final color = _getTransactionColor(history.transactionType);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 타임라인 인디케이터
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Column(
|
||||
children: [
|
||||
// 위쪽 연결선
|
||||
if (!isFirst)
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
),
|
||||
// 원형 인디케이터
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: color,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: color.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
_getTransactionIcon(history.transactionType),
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
// 아래쪽 연결선
|
||||
if (!isLast)
|
||||
Container(
|
||||
width: 2,
|
||||
height: 20,
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 이력 정보
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: isLast ? 0 : 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: color.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더 (거래 유형 + 날짜)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ShadcnBadge(
|
||||
text: history.transactionTypeDisplay,
|
||||
variant: _getBadgeVariant(history.transactionType),
|
||||
size: ShadcnBadgeSize.small,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isFirst)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'최근',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
history.formattedDate,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 위치 정보
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
history.isCustomerLocation ? Icons.business : Icons.warehouse,
|
||||
size: 16,
|
||||
color: history.isCustomerLocation ? Colors.blue : Colors.green,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'위치: ',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
history.location,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 수량 정보
|
||||
if (history.quantity > 0) ...[
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory,
|
||||
size: 16,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'수량: ',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${history.quantity}개',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
// 비고
|
||||
if (history.remark != null && history.remark!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.note,
|
||||
size: 14,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
history.remark!,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Badge Variant 반환
|
||||
ShadcnBadgeVariant _getBadgeVariant(String transactionType) {
|
||||
switch (transactionType) {
|
||||
case 'I':
|
||||
return ShadcnBadgeVariant.success; // 입고
|
||||
case 'O':
|
||||
return ShadcnBadgeVariant.warning; // 출고
|
||||
case 'R':
|
||||
return ShadcnBadgeVariant.info; // 대여
|
||||
case 'D':
|
||||
return ShadcnBadgeVariant.destructive; // 폐기
|
||||
default:
|
||||
return ShadcnBadgeVariant.secondary;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('장비 이력 상세'),
|
||||
],
|
||||
),
|
||||
description: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: 600,
|
||||
height: 500,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 장비 정보 헤더
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.precision_manufacturing,
|
||||
color: ShadcnTheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.equipmentName,
|
||||
style: ShadcnTheme.bodyLarge.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.qr_code,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'시리얼: ${widget.serialNumber}',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// 이력 목록 헤더
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timeline,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'변동 이력 (시간순)',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
if (_historyList != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
ShadcnBadge(
|
||||
text: '${_historyList!.length}건',
|
||||
variant: ShadcnBadgeVariant.secondary,
|
||||
size: ShadcnBadgeSize.small,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 이력 목록
|
||||
Expanded(
|
||||
child: _buildHistoryContent(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
ShadcnButton(
|
||||
text: '새로고침',
|
||||
onPressed: _loadEquipmentHistory,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
),
|
||||
ShadcnButton(
|
||||
text: '닫기',
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 이력 컨텐츠 빌더
|
||||
Widget _buildHistoryContent() {
|
||||
if (_isLoading) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 32,
|
||||
height: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text('이력을 불러오는 중...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.red.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'이력을 불러올 수 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_error!,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ShadcnButton(
|
||||
text: '다시 시도',
|
||||
onPressed: _loadEquipmentHistory,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_historyList == null || _historyList!.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 48,
|
||||
color: ShadcnTheme.mutedForeground.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'등록된 이력이 없습니다',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: _historyList!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildTimelineItem(_historyList![index], index);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../screens/equipment/controllers/equipment_history_controller.dart';
|
||||
import 'components/transaction_type_badge.dart';
|
||||
import '../common/layouts/base_list_screen.dart';
|
||||
import '../common/widgets/standard_action_bar.dart';
|
||||
import '../common/widgets/pagination.dart';
|
||||
import 'package:superport/screens/inventory/controllers/inventory_history_controller.dart';
|
||||
import 'package:superport/data/models/inventory_history_view_model.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/common/widgets/standard_action_bar.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/inventory/dialogs/equipment_history_detail_dialog.dart';
|
||||
|
||||
/// 재고 이력 관리 화면 (완전 재설계)
|
||||
/// 요구사항: 장비명, 시리얼번호, 위치, 변동일, 작업, 비고
|
||||
class InventoryHistoryScreen extends StatefulWidget {
|
||||
const InventoryHistoryScreen({super.key});
|
||||
|
||||
@@ -16,46 +20,58 @@ class InventoryHistoryScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
late final InventoryHistoryController _controller;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _appliedSearchKeyword = '';
|
||||
String _selectedType = 'all';
|
||||
String _selectedTransactionType = 'all';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = InventoryHistoryController();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<EquipmentHistoryController>().loadHistory();
|
||||
_controller.loadHistories();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 검색 실행
|
||||
void _onSearch() {
|
||||
final searchQuery = _searchController.text.trim();
|
||||
setState(() {
|
||||
_appliedSearchKeyword = searchQuery;
|
||||
});
|
||||
// ✅ Controller 검색 메서드 연동
|
||||
context.read<EquipmentHistoryController>().setFilters(
|
||||
searchQuery: searchQuery.isNotEmpty ? searchQuery : null,
|
||||
transactionType: _selectedType != 'all' ? _selectedType : null,
|
||||
|
||||
_controller.setFilters(
|
||||
searchKeyword: searchQuery.isNotEmpty ? searchQuery : null,
|
||||
transactionType: _selectedTransactionType != 'all' ? _selectedTransactionType : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 검색 초기화
|
||||
void _clearSearch() {
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
_appliedSearchKeyword = '';
|
||||
_selectedType = 'all';
|
||||
_selectedTransactionType = 'all';
|
||||
});
|
||||
// ✅ Controller 필터 초기화
|
||||
context.read<EquipmentHistoryController>().setFilters(
|
||||
searchQuery: null,
|
||||
transactionType: null,
|
||||
_controller.clearFilters();
|
||||
}
|
||||
|
||||
/// 거래 유형 필터 변경
|
||||
void _onTransactionTypeChanged(String type) {
|
||||
setState(() {
|
||||
_selectedTransactionType = type;
|
||||
});
|
||||
_controller.setFilters(
|
||||
searchKeyword: _appliedSearchKeyword.isNotEmpty ? _appliedSearchKeyword : null,
|
||||
transactionType: type != 'all' ? type : null,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,12 +82,16 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
required bool useExpanded,
|
||||
required double minWidth,
|
||||
}) {
|
||||
final theme = ShadTheme.of(context);
|
||||
final child = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.large.copyWith(fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -91,6 +111,7 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
}) {
|
||||
final container = Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: child,
|
||||
);
|
||||
|
||||
@@ -101,144 +122,150 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 헤더 셀 리스트
|
||||
/// 헤더 셀 리스트 (요구사항에 맞게 재정의)
|
||||
List<Widget> _buildHeaderCells() {
|
||||
return [
|
||||
_buildHeaderCell('ID', flex: 0, useExpanded: false, minWidth: 60),
|
||||
_buildHeaderCell('거래 유형', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('장비명', flex: 2, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('시리얼 번호', flex: 2, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('창고', flex: 1, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('수량', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('거래일', flex: 0, useExpanded: false, minWidth: 100),
|
||||
_buildHeaderCell('비고', flex: 1, useExpanded: true, minWidth: 100),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 100),
|
||||
_buildHeaderCell('장비명', flex: 3, useExpanded: true, minWidth: 150),
|
||||
_buildHeaderCell('시리얼번호', flex: 2, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('위치', flex: 2, useExpanded: true, minWidth: 120),
|
||||
_buildHeaderCell('변동일', flex: 1, useExpanded: false, minWidth: 100),
|
||||
_buildHeaderCell('작업', flex: 0, useExpanded: false, minWidth: 80),
|
||||
_buildHeaderCell('비고', flex: 2, useExpanded: true, minWidth: 120),
|
||||
];
|
||||
}
|
||||
|
||||
/// 테이블 행 빌더
|
||||
Widget _buildTableRow(dynamic history, int index) {
|
||||
final theme = ShadTheme.of(context);
|
||||
/// 테이블 행 빌더 (요구사항에 맞게 재정의)
|
||||
Widget _buildTableRow(InventoryHistoryViewModel history, int index) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index.isEven
|
||||
? theme.colorScheme.muted.withValues(alpha: 0.1)
|
||||
: null,
|
||||
color: index.isEven ? ShadcnTheme.muted.withValues(alpha: 0.1) : null,
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
bottom: BorderSide(color: Colors.black12, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 장비명
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${history.id}',
|
||||
style: theme.textTheme.small,
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 60,
|
||||
),
|
||||
_buildDataCell(
|
||||
TransactionTypeBadge(
|
||||
type: history.transactionType ?? '',
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
history.equipment?.modelName ?? '-',
|
||||
style: theme.textTheme.large.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
Tooltip(
|
||||
message: history.equipmentName,
|
||||
child: Text(
|
||||
history.equipmentName,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: true,
|
||||
minWidth: 150,
|
||||
),
|
||||
// 시리얼번호
|
||||
_buildDataCell(
|
||||
Tooltip(
|
||||
message: history.serialNumber,
|
||||
child: Text(
|
||||
history.serialNumber,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 위치 (출고/대여: 고객사, 입고/폐기: 창고)
|
||||
_buildDataCell(
|
||||
Text(
|
||||
history.equipment?.serialNumber ?? '-',
|
||||
style: theme.textTheme.small,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Tooltip(
|
||||
message: history.location,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
history.isCustomerLocation ? Icons.business : Icons.warehouse,
|
||||
size: 14,
|
||||
color: history.isCustomerLocation ? Colors.blue : Colors.green,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
history.location,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 변동일
|
||||
_buildDataCell(
|
||||
Text(
|
||||
history.warehouse?.name ?? '-',
|
||||
style: theme.textTheme.small,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
history.formattedDate,
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: true,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
),
|
||||
// 작업 (상세보기만)
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${history.quantity ?? 0}',
|
||||
style: theme.textTheme.small,
|
||||
textAlign: TextAlign.center,
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDetail(history),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.history, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text('상세보기', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 비고
|
||||
_buildDataCell(
|
||||
Text(
|
||||
DateFormat('yyyy-MM-dd').format(history.transactedAt),
|
||||
style: theme.textTheme.small,
|
||||
Tooltip(
|
||||
message: history.remark ?? '비고 없음',
|
||||
child: Text(
|
||||
history.remark ?? '-',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Text(
|
||||
history.remark ?? '-',
|
||||
style: theme.textTheme.small,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
flex: 1,
|
||||
flex: 2,
|
||||
useExpanded: true,
|
||||
minWidth: 100,
|
||||
),
|
||||
_buildDataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () {
|
||||
// 편집 기능
|
||||
},
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () {
|
||||
// 삭제 기능
|
||||
},
|
||||
child: const Icon(Icons.delete, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 0,
|
||||
useExpanded: false,
|
||||
minWidth: 100,
|
||||
minWidth: 120,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 장비 이력 상세보기 다이얼로그 표시
|
||||
void _showEquipmentHistoryDetail(InventoryHistoryViewModel history) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return EquipmentHistoryDetailDialog(
|
||||
equipmentId: history.equipmentId,
|
||||
equipmentName: history.equipmentName,
|
||||
serialNumber: history.serialNumber,
|
||||
controller: _controller,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 검색 바 빌더
|
||||
Widget _buildSearchBar() {
|
||||
return Row(
|
||||
@@ -249,23 +276,23 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
child: Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: ShadTheme.of(context).colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.black),
|
||||
color: ShadcnTheme.card,
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onSubmitted: (_) => _onSearch(),
|
||||
decoration: InputDecoration(
|
||||
hintText: '장비명, 시리얼번호, 창고명 등...',
|
||||
hintText: '장비명, 시리얼번호, 위치, 비고 등...',
|
||||
hintStyle: TextStyle(
|
||||
color: ShadTheme.of(context).colorScheme.mutedForeground.withValues(alpha: 0.8),
|
||||
color: ShadcnTheme.mutedForeground.withValues(alpha: 0.8),
|
||||
fontSize: 14),
|
||||
prefixIcon: Icon(Icons.search, color: ShadTheme.of(context).colorScheme.muted, size: 20),
|
||||
prefixIcon: Icon(Icons.search, color: ShadcnTheme.muted, size: 20),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
style: ShadTheme.of(context).textTheme.large,
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -273,36 +300,27 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 거래 유형 필터
|
||||
Container(
|
||||
SizedBox(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadTheme.of(context).colorScheme.card,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.black),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedType,
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'all', child: Text('전체')),
|
||||
DropdownMenuItem(value: 'I', child: Text('입고')),
|
||||
DropdownMenuItem(value: 'O', child: Text('출고')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
});
|
||||
// ✅ 필터 변경 시 즉시 Controller에 반영
|
||||
context.read<EquipmentHistoryController>().setFilters(
|
||||
searchQuery: _appliedSearchKeyword.isNotEmpty ? _appliedSearchKeyword : null,
|
||||
transactionType: value != 'all' ? value : null,
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ShadTheme.of(context).textTheme.large,
|
||||
width: 120,
|
||||
child: ShadSelect<String>(
|
||||
selectedOptionBuilder: (context, value) => Text(
|
||||
_getTransactionTypeDisplayText(value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
placeholder: const Text('거래 유형'),
|
||||
options: [
|
||||
const ShadOption(value: 'all', child: Text('전체')),
|
||||
const ShadOption(value: 'I', child: Text('입고')),
|
||||
const ShadOption(value: 'O', child: Text('출고')),
|
||||
const ShadOption(value: 'R', child: Text('대여')),
|
||||
const ShadOption(value: 'D', child: Text('폐기')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
_onTransactionTypeChanged(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -311,19 +329,24 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
// 검색 버튼
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ShadButton(
|
||||
child: ShadcnButton(
|
||||
text: '검색',
|
||||
onPressed: _onSearch,
|
||||
child: const Text('검색'),
|
||||
variant: ShadcnButtonVariant.primary,
|
||||
textColor: Colors.white,
|
||||
icon: const Icon(Icons.search, size: 16),
|
||||
),
|
||||
),
|
||||
|
||||
if (_appliedSearchKeyword.isNotEmpty) ...[
|
||||
if (_appliedSearchKeyword.isNotEmpty || _selectedTransactionType != 'all') ...[
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ShadButton.outline(
|
||||
child: ShadcnButton(
|
||||
text: '초기화',
|
||||
onPressed: _clearSearch,
|
||||
child: const Text('초기화'),
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.clear, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -333,89 +356,84 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
|
||||
/// 액션 바 빌더
|
||||
Widget _buildActionBar() {
|
||||
return Consumer<EquipmentHistoryController>(
|
||||
return Consumer<InventoryHistoryController>(
|
||||
builder: (context, controller, child) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 제목과 설명
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'재고 이력 관리',
|
||||
style: ShadTheme.of(context).textTheme.h4,
|
||||
final stats = controller.getStatistics();
|
||||
return StandardActionBar(
|
||||
leftActions: [
|
||||
// 통계 정보 표시
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.inventory_2, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'총 ${stats['total']}건',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'장비 입출고 이력을 조회하고 관리합니다',
|
||||
style: ShadTheme.of(context).textTheme.muted,
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ShadButton(
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/inventory/stock-in');
|
||||
},
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.add, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('입고 등록'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.outline(
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/inventory/stock-out');
|
||||
},
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.remove, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Text('출고 처리'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 표준 액션바
|
||||
StandardActionBar(
|
||||
totalCount: controller.totalCount,
|
||||
statusMessage: '총 ${controller.totalTransactions}건의 거래 이력',
|
||||
rightActions: [
|
||||
ShadButton.ghost(
|
||||
onPressed: () => controller.loadHistory(),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.refresh, size: 16),
|
||||
SizedBox(width: 4),
|
||||
Text('새로고침'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (controller.hasActiveFilters) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Text('|', style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'필터링됨',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: Colors.orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
rightActions: [
|
||||
// 새로고침 버튼
|
||||
ShadcnButton(
|
||||
text: '새로고침',
|
||||
onPressed: () => controller.refresh(),
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.refresh, size: 16),
|
||||
),
|
||||
],
|
||||
totalCount: stats['total'],
|
||||
statusMessage: controller.hasActiveFilters
|
||||
? '${controller.filterStatusText}'
|
||||
: '장비 입출고 이력을 조회합니다',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 데이터 테이블 빌더 (표준 패턴)
|
||||
Widget _buildDataTable(List<dynamic> historyList) {
|
||||
/// 거래 유형 표시 텍스트
|
||||
String _getTransactionTypeDisplayText(String type) {
|
||||
switch (type) {
|
||||
case 'all':
|
||||
return '전체';
|
||||
case 'I':
|
||||
return '입고';
|
||||
case 'O':
|
||||
return '출고';
|
||||
case 'R':
|
||||
return '대여';
|
||||
case 'D':
|
||||
return '폐기';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/// 데이터 테이블 빌더
|
||||
Widget _buildDataTable(List<InventoryHistoryViewModel> historyList) {
|
||||
if (historyList.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -424,17 +442,24 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 64,
|
||||
color: ShadTheme.of(context).colorScheme.mutedForeground,
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_appliedSearchKeyword.isNotEmpty
|
||||
? '검색 결과가 없습니다'
|
||||
_appliedSearchKeyword.isNotEmpty || _selectedTransactionType != 'all'
|
||||
? '검색 조건에 맞는 이력이 없습니다'
|
||||
: '등록된 재고 이력이 없습니다',
|
||||
style: ShadTheme.of(context).textTheme.large.copyWith(
|
||||
color: ShadTheme.of(context).colorScheme.mutedForeground,
|
||||
style: ShadcnTheme.bodyLarge.copyWith(
|
||||
color: ShadcnTheme.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_appliedSearchKeyword.isNotEmpty || _selectedTransactionType != 'all')
|
||||
ShadcnButton(
|
||||
text: '필터 초기화',
|
||||
onPressed: _clearSearch,
|
||||
variant: ShadcnButtonVariant.secondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -443,17 +468,23 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.black),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ShadcnTheme.border),
|
||||
borderRadius: BorderRadius.circular(ShadcnTheme.radiusMd),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 고정 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadTheme.of(context).colorScheme.muted.withValues(alpha: 0.3),
|
||||
border: const Border(bottom: BorderSide(color: Colors.black)),
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: const Border(
|
||||
bottom: BorderSide(color: Colors.black12),
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Row(children: _buildHeaderCells()),
|
||||
),
|
||||
@@ -472,39 +503,40 @@ class _InventoryHistoryScreenState extends State<InventoryHistoryScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<EquipmentHistoryController>(
|
||||
builder: (context, controller, child) {
|
||||
return BaseListScreen(
|
||||
isLoading: controller.isLoading && controller.historyList.isEmpty,
|
||||
error: controller.error,
|
||||
onRefresh: () => controller.loadHistory(),
|
||||
emptyMessage: _appliedSearchKeyword.isNotEmpty
|
||||
? '검색 결과가 없습니다'
|
||||
: '등록된 재고 이력이 없습니다',
|
||||
emptyIcon: Icons.inventory_2_outlined,
|
||||
return ChangeNotifierProvider<InventoryHistoryController>.value(
|
||||
value: _controller,
|
||||
child: Consumer<InventoryHistoryController>(
|
||||
builder: (context, controller, child) {
|
||||
return BaseListScreen(
|
||||
isLoading: controller.isLoading && controller.historyItems.isEmpty,
|
||||
error: controller.error,
|
||||
onRefresh: () => controller.refresh(),
|
||||
emptyMessage: _appliedSearchKeyword.isNotEmpty || _selectedTransactionType != 'all'
|
||||
? '검색 조건에 맞는 이력이 없습니다'
|
||||
: '등록된 재고 이력이 없습니다',
|
||||
emptyIcon: Icons.inventory_2_outlined,
|
||||
|
||||
// 검색바
|
||||
searchBar: _buildSearchBar(),
|
||||
// 검색바
|
||||
searchBar: _buildSearchBar(),
|
||||
|
||||
// 액션바
|
||||
actionBar: _buildActionBar(),
|
||||
// 액션바
|
||||
actionBar: _buildActionBar(),
|
||||
|
||||
// 데이터 테이블
|
||||
dataTable: _buildDataTable(controller.historyList),
|
||||
// 데이터 테이블
|
||||
dataTable: _buildDataTable(controller.historyItems),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: controller.totalPages > 1
|
||||
? Pagination(
|
||||
totalCount: controller.totalCount,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: AppConstants.historyPageSize, // controller.pageSize 대신 고정값 사용
|
||||
onPageChanged: (page) => {
|
||||
// 페이지 변경 로직 - 추후 Controller에 추가 예정
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
// 페이지네이션
|
||||
pagination: controller.totalPages > 1
|
||||
? Pagination(
|
||||
totalCount: controller.totalCount,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
onPageChanged: (page) => controller.goToPage(page),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user