사용하지 않는 파일 정리 전 백업 (Phase 10 완료 후 상태)
This commit is contained in:
414
lib/screens/equipment/controllers/equipment_form_controller.dart
Normal file
414
lib/screens/equipment/controllers/equipment_form_controller.dart
Normal file
@@ -0,0 +1,414 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../../data/models/equipment/equipment_dto.dart';
|
||||
import '../../../data/models/company/company_dto.dart';
|
||||
import '../../../data/models/model_dto.dart';
|
||||
import '../../../domain/usecases/equipment/create_equipment_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/update_equipment_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/get_equipment_detail_usecase.dart';
|
||||
import '../../../domain/usecases/company/get_companies_usecase.dart';
|
||||
import '../../../domain/usecases/model_usecase.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
|
||||
/// 장비 폼 컨트롤러 (생성/수정)
|
||||
/// Phase 8 - Clean Architecture 패턴, 복합 FK 지원
|
||||
@injectable
|
||||
class EquipmentFormController extends ChangeNotifier {
|
||||
final CreateEquipmentUseCase _createEquipmentUseCase;
|
||||
final UpdateEquipmentUseCase _updateEquipmentUseCase;
|
||||
final GetEquipmentDetailUseCase _getEquipmentDetailUseCase;
|
||||
final GetCompaniesUseCase _getCompaniesUseCase;
|
||||
final ModelUseCase _modelUseCase;
|
||||
|
||||
EquipmentFormController(
|
||||
this._createEquipmentUseCase,
|
||||
this._updateEquipmentUseCase,
|
||||
this._getEquipmentDetailUseCase,
|
||||
this._getCompaniesUseCase,
|
||||
this._modelUseCase,
|
||||
);
|
||||
|
||||
// 상태 관리
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingCompanies = false;
|
||||
bool _isLoadingModels = false;
|
||||
bool _isSaving = false;
|
||||
String? _error;
|
||||
|
||||
// 폼 데이터
|
||||
EquipmentDto? _currentEquipment;
|
||||
int? _equipmentId; // 수정 모드일 때 사용
|
||||
|
||||
// 드롭다운 데이터
|
||||
List<CompanyDto> _companies = [];
|
||||
List<ModelDto> _models = [];
|
||||
List<ModelDto> _filteredModels = [];
|
||||
|
||||
// 선택된 값
|
||||
int? _selectedCompanyId;
|
||||
int? _selectedModelId;
|
||||
|
||||
// 폼 컨트롤러들
|
||||
final TextEditingController serialNumberController = TextEditingController();
|
||||
final TextEditingController barcodeController = TextEditingController();
|
||||
final TextEditingController purchasePriceController = TextEditingController();
|
||||
final TextEditingController warrantyNumberController = TextEditingController();
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
// 날짜 필드들
|
||||
DateTime? _purchasedAt;
|
||||
DateTime _warrantyStartedAt = DateTime.now();
|
||||
DateTime _warrantyEndedAt = DateTime.now().add(const Duration(days: 365));
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingCompanies => _isLoadingCompanies;
|
||||
bool get isLoadingModels => _isLoadingModels;
|
||||
bool get isSaving => _isSaving;
|
||||
String? get error => _error;
|
||||
EquipmentDto? get currentEquipment => _currentEquipment;
|
||||
bool get isEditMode => _equipmentId != null;
|
||||
|
||||
List<CompanyDto> get companies => _companies;
|
||||
List<ModelDto> get filteredModels => _filteredModels;
|
||||
|
||||
int? get selectedCompanyId => _selectedCompanyId;
|
||||
int? get selectedModelId => _selectedModelId;
|
||||
|
||||
DateTime? get purchasedAt => _purchasedAt;
|
||||
DateTime get warrantyStartedAt => _warrantyStartedAt;
|
||||
DateTime get warrantyEndedAt => _warrantyEndedAt;
|
||||
|
||||
/// 초기화 (생성 모드)
|
||||
Future<void> initializeForCreate() async {
|
||||
_equipmentId = null;
|
||||
_currentEquipment = null;
|
||||
_clearForm();
|
||||
await _loadInitialData();
|
||||
}
|
||||
|
||||
/// 초기화 (수정 모드)
|
||||
Future<void> initializeForEdit(int equipmentId) async {
|
||||
_equipmentId = equipmentId;
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _loadInitialData();
|
||||
await _loadEquipmentDetail(equipmentId);
|
||||
} catch (e) {
|
||||
_error = '장비 정보를 불러오는데 실패했습니다: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 초기 데이터 로드 (회사, 모델)
|
||||
Future<void> _loadInitialData() async {
|
||||
await Future.wait([
|
||||
_loadCompanies(),
|
||||
_loadModels(),
|
||||
]);
|
||||
}
|
||||
|
||||
/// 회사 목록 로드
|
||||
Future<void> _loadCompanies() async {
|
||||
_isLoadingCompanies = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final params = GetCompaniesParams(page: 1, perPage: 1000); // 모든 회사 가져오기
|
||||
final result = await _getCompaniesUseCase(params);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_error = _getErrorMessage(failure);
|
||||
},
|
||||
(paginatedResponse) {
|
||||
_companies = paginatedResponse.items
|
||||
.cast<CompanyDto>() // 타입 캐스팅 추가
|
||||
.where((company) => company.isActive)
|
||||
.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_error = '회사 목록을 불러오는데 실패했습니다: $e';
|
||||
} finally {
|
||||
_isLoadingCompanies = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모델 목록 로드
|
||||
Future<void> _loadModels() async {
|
||||
_isLoadingModels = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_models = await _modelUseCase.getModels();
|
||||
_filteredModels = _models;
|
||||
} catch (e) {
|
||||
_error = '모델 목록을 불러오는데 실패했습니다: $e';
|
||||
} finally {
|
||||
_isLoadingModels = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 상세 정보 로드 (수정 모드)
|
||||
Future<void> _loadEquipmentDetail(int equipmentId) async {
|
||||
final result = await _getEquipmentDetailUseCase(equipmentId);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_error = _getErrorMessage(failure);
|
||||
},
|
||||
(equipment) {
|
||||
_currentEquipment = equipment;
|
||||
_populateForm(equipment);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 폼 필드에 데이터 채우기 (수정 모드)
|
||||
void _populateForm(EquipmentDto equipment) {
|
||||
_selectedCompanyId = equipment.companiesId;
|
||||
_selectedModelId = equipment.modelsId;
|
||||
|
||||
serialNumberController.text = equipment.serialNumber;
|
||||
barcodeController.text = equipment.barcode ?? '';
|
||||
purchasePriceController.text = equipment.purchasePrice.toString();
|
||||
warrantyNumberController.text = equipment.warrantyNumber;
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
|
||||
_purchasedAt = equipment.purchasedAt;
|
||||
_warrantyStartedAt = equipment.warrantyStartedAt;
|
||||
_warrantyEndedAt = equipment.warrantyEndedAt;
|
||||
|
||||
// 선택된 회사에 따라 모델 필터링
|
||||
_filterModelsByCompany(_selectedCompanyId);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 회사 선택
|
||||
void selectCompany(int? companyId) {
|
||||
_selectedCompanyId = companyId;
|
||||
_selectedModelId = null; // 모델 선택 초기화
|
||||
_filterModelsByCompany(companyId);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 모델 선택
|
||||
void selectModel(int? modelId) {
|
||||
_selectedModelId = modelId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 회사별 모델 필터링
|
||||
void _filterModelsByCompany(int? companyId) {
|
||||
if (companyId == null) {
|
||||
_filteredModels = _models;
|
||||
} else {
|
||||
// 실제로는 vendor로 필터링해야 하지만,
|
||||
// 현재 구조에서는 모든 모델을 보여주고 사용자가 선택하도록 함
|
||||
_filteredModels = _models;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 구매일 선택
|
||||
void setPurchasedAt(DateTime? date) {
|
||||
_purchasedAt = date;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 워런티 시작일 선택
|
||||
void setWarrantyStartedAt(DateTime date) {
|
||||
_warrantyStartedAt = date;
|
||||
// 시작일이 종료일보다 늦으면 종료일을 1년 후로 설정
|
||||
if (_warrantyStartedAt.isAfter(_warrantyEndedAt)) {
|
||||
_warrantyEndedAt = _warrantyStartedAt.add(const Duration(days: 365));
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 워런티 종료일 선택
|
||||
void setWarrantyEndedAt(DateTime date) {
|
||||
_warrantyEndedAt = date;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 폼 유효성 검증
|
||||
String? validateForm() {
|
||||
if (_selectedCompanyId == null) {
|
||||
return '회사를 선택해주세요.';
|
||||
}
|
||||
|
||||
if (_selectedModelId == null) {
|
||||
return '모델을 선택해주세요.';
|
||||
}
|
||||
|
||||
if (serialNumberController.text.trim().isEmpty) {
|
||||
return '시리얼 번호를 입력해주세요.';
|
||||
}
|
||||
|
||||
if (warrantyNumberController.text.trim().isEmpty) {
|
||||
return '워런티 번호를 입력해주세요.';
|
||||
}
|
||||
|
||||
if (_warrantyStartedAt.isAfter(_warrantyEndedAt)) {
|
||||
return '워런티 시작일이 종료일보다 늦을 수 없습니다.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 장비 저장 (생성 또는 수정)
|
||||
Future<bool> saveEquipment() async {
|
||||
final validationError = validateForm();
|
||||
if (validationError != null) {
|
||||
_error = validationError;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
|
||||
_isSaving = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (isEditMode) {
|
||||
return await _updateEquipment();
|
||||
} else {
|
||||
return await _createEquipment();
|
||||
}
|
||||
} catch (e) {
|
||||
_error = '저장 중 오류가 발생했습니다: $e';
|
||||
return false;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 생성
|
||||
Future<bool> _createEquipment() async {
|
||||
final request = EquipmentRequestDto(
|
||||
companiesId: _selectedCompanyId!,
|
||||
modelsId: _selectedModelId!,
|
||||
serialNumber: serialNumberController.text.trim(),
|
||||
barcode: barcodeController.text.trim().isNotEmpty
|
||||
? barcodeController.text.trim()
|
||||
: null,
|
||||
purchasedAt: _purchasedAt,
|
||||
purchasePrice: int.tryParse(purchasePriceController.text) ?? 0,
|
||||
warrantyNumber: warrantyNumberController.text.trim(),
|
||||
warrantyStartedAt: _warrantyStartedAt,
|
||||
warrantyEndedAt: _warrantyEndedAt,
|
||||
remark: remarkController.text.trim().isNotEmpty
|
||||
? remarkController.text.trim()
|
||||
: null,
|
||||
);
|
||||
|
||||
final result = await _createEquipmentUseCase(request);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
_error = _getErrorMessage(failure);
|
||||
return false;
|
||||
},
|
||||
(equipment) {
|
||||
_currentEquipment = equipment;
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 장비 수정
|
||||
Future<bool> _updateEquipment() async {
|
||||
final request = EquipmentUpdateRequestDto(
|
||||
companiesId: _selectedCompanyId!,
|
||||
modelsId: _selectedModelId!,
|
||||
serialNumber: serialNumberController.text.trim(),
|
||||
barcode: barcodeController.text.trim().isNotEmpty
|
||||
? barcodeController.text.trim()
|
||||
: null,
|
||||
purchasedAt: _purchasedAt,
|
||||
purchasePrice: int.tryParse(purchasePriceController.text) ?? 0,
|
||||
warrantyNumber: warrantyNumberController.text.trim(),
|
||||
warrantyStartedAt: _warrantyStartedAt,
|
||||
warrantyEndedAt: _warrantyEndedAt,
|
||||
remark: remarkController.text.trim().isNotEmpty
|
||||
? remarkController.text.trim()
|
||||
: null,
|
||||
);
|
||||
|
||||
final result = await _updateEquipmentUseCase(UpdateEquipmentParams(
|
||||
id: _equipmentId!,
|
||||
request: request,
|
||||
));
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
_error = _getErrorMessage(failure);
|
||||
return false;
|
||||
},
|
||||
(equipment) {
|
||||
_currentEquipment = equipment;
|
||||
return true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 폼 초기화
|
||||
void _clearForm() {
|
||||
_selectedCompanyId = null;
|
||||
_selectedModelId = null;
|
||||
|
||||
serialNumberController.clear();
|
||||
barcodeController.clear();
|
||||
purchasePriceController.text = '0';
|
||||
warrantyNumberController.clear();
|
||||
remarkController.clear();
|
||||
|
||||
_purchasedAt = null;
|
||||
_warrantyStartedAt = DateTime.now();
|
||||
_warrantyEndedAt = DateTime.now().add(const Duration(days: 365));
|
||||
|
||||
_error = null;
|
||||
}
|
||||
|
||||
/// 에러 메시지 변환
|
||||
String _getErrorMessage(Failure failure) {
|
||||
switch (failure.runtimeType) {
|
||||
case ServerFailure:
|
||||
return (failure as ServerFailure).message;
|
||||
case NetworkFailure:
|
||||
return '네트워크 연결을 확인해주세요.';
|
||||
case ValidationFailure:
|
||||
return (failure as ValidationFailure).message;
|
||||
default:
|
||||
return '알 수 없는 오류가 발생했습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
/// 에러 초기화
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
serialNumberController.dispose();
|
||||
barcodeController.dispose();
|
||||
purchasePriceController.dispose();
|
||||
warrantyNumberController.dispose();
|
||||
remarkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../data/models/equipment_history_dto.dart';
|
||||
import '../../../domain/usecases/equipment_history_usecase.dart';
|
||||
import '../../../utils/constants.dart';
|
||||
|
||||
class EquipmentHistoryController extends ChangeNotifier {
|
||||
final EquipmentHistoryUseCase _useCase;
|
||||
|
||||
EquipmentHistoryController({
|
||||
EquipmentHistoryUseCase? useCase,
|
||||
}) : _useCase = useCase ?? GetIt.instance<EquipmentHistoryUseCase>();
|
||||
|
||||
// 상태 변수 (백엔드 스키마 기반)
|
||||
List<EquipmentHistoryDto> _historyList = [];
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _pageSize = PaginationConstants.defaultPageSize;
|
||||
int _totalCount = 0;
|
||||
|
||||
// 필터 (백엔드 실제 필드만)
|
||||
String _searchQuery = '';
|
||||
String? _transactionType;
|
||||
int? _warehouseId;
|
||||
int? _equipmentId;
|
||||
DateTime? _startDate;
|
||||
DateTime? _endDate;
|
||||
|
||||
// Getters (백엔드 실제 데이터만)
|
||||
List<EquipmentHistoryDto> get historyList => _historyList;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
int get currentPage => _currentPage;
|
||||
int get totalPages => (_totalCount / _pageSize).ceil();
|
||||
int get totalCount => _totalCount;
|
||||
String get searchQuery => _searchQuery;
|
||||
|
||||
// 간단한 통계 (백엔드 실제 데이터 기반)
|
||||
int get totalTransactions => _historyList.length;
|
||||
int get totalInQuantity => _historyList
|
||||
.where((item) => item.transactionType == 'I')
|
||||
.fold(0, (sum, item) => sum + item.quantity);
|
||||
int get totalOutQuantity => _historyList
|
||||
.where((item) => item.transactionType == 'O')
|
||||
.fold(0, (sum, item) => sum + item.quantity);
|
||||
|
||||
// 재고 이력 조회
|
||||
Future<void> loadHistory({bool refresh = false}) async {
|
||||
if (refresh) {
|
||||
_currentPage = 1;
|
||||
_historyList.clear();
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
transactionType: _transactionType,
|
||||
warehousesId: _warehouseId,
|
||||
equipmentsId: _equipmentId,
|
||||
startDate: _startDate?.toIso8601String(),
|
||||
endDate: _endDate?.toIso8601String(),
|
||||
);
|
||||
|
||||
// result는 EquipmentHistoryListResponse 타입으로 Either가 아님
|
||||
if (refresh) {
|
||||
_historyList = result.items;
|
||||
} else {
|
||||
_historyList.addAll(result.items);
|
||||
}
|
||||
_totalCount = result.totalCount;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 백엔드 스키마 기반 단순 재고 조회 (EquipmentHistoryDto 기반)
|
||||
// 특정 장비의 현재 재고량 계산 (입고량 - 출고량)
|
||||
int calculateEquipmentStock(int equipmentId) {
|
||||
final equipmentHistories = _historyList.where((h) => h.equipmentsId == equipmentId);
|
||||
int totalIn = equipmentHistories
|
||||
.where((h) => h.transactionType == 'I')
|
||||
.fold(0, (sum, h) => sum + h.quantity);
|
||||
int totalOut = equipmentHistories
|
||||
.where((h) => h.transactionType == 'O')
|
||||
.fold(0, (sum, h) => sum + h.quantity);
|
||||
return totalIn - totalOut;
|
||||
}
|
||||
|
||||
// 백엔드 스키마 기반 단순 필터링
|
||||
void setFilters({
|
||||
String? transactionType,
|
||||
int? warehouseId,
|
||||
int? equipmentId,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
String? searchQuery,
|
||||
}) {
|
||||
_transactionType = transactionType;
|
||||
_warehouseId = warehouseId;
|
||||
_equipmentId = equipmentId;
|
||||
_startDate = startDate;
|
||||
_endDate = endDate;
|
||||
if (searchQuery != null) _searchQuery = searchQuery;
|
||||
|
||||
_currentPage = 1;
|
||||
loadHistory(refresh: true);
|
||||
}
|
||||
|
||||
void clearFilters() {
|
||||
_transactionType = null;
|
||||
_warehouseId = null;
|
||||
_equipmentId = null;
|
||||
_startDate = null;
|
||||
_endDate = null;
|
||||
_searchQuery = '';
|
||||
|
||||
_currentPage = 1;
|
||||
loadHistory(refresh: true);
|
||||
}
|
||||
|
||||
// 백엔드 스키마 기반 단순 통계
|
||||
Map<String, int> getStockSummary() {
|
||||
int totalIn = _historyList
|
||||
.where((h) => h.transactionType == 'I')
|
||||
.fold(0, (sum, h) => sum + h.quantity);
|
||||
int totalOut = _historyList
|
||||
.where((h) => h.transactionType == 'O')
|
||||
.fold(0, (sum, h) => sum + h.quantity);
|
||||
|
||||
return {
|
||||
'totalStock': totalIn - totalOut,
|
||||
'totalIn': totalIn,
|
||||
'totalOut': totalOut,
|
||||
};
|
||||
}
|
||||
|
||||
// 입고 처리 (백엔드 스키마 기반)
|
||||
Future<bool> createStockIn({
|
||||
required int equipmentsId,
|
||||
required int warehousesId,
|
||||
required int quantity,
|
||||
DateTime? transactedAt,
|
||||
String? remark,
|
||||
}) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final request = EquipmentHistoryRequestDto(
|
||||
equipmentsId: equipmentsId,
|
||||
warehousesId: warehousesId,
|
||||
transactionType: 'I', // 입고
|
||||
quantity: quantity,
|
||||
transactedAt: transactedAt ?? DateTime.now(),
|
||||
remark: remark,
|
||||
);
|
||||
|
||||
await _useCase.createEquipmentHistory(request);
|
||||
await loadHistory(refresh: true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 출고 처리 (백엔드 스키마 기반)
|
||||
Future<bool> createStockOut({
|
||||
required int equipmentsId,
|
||||
required int warehousesId,
|
||||
required int quantity,
|
||||
DateTime? transactedAt,
|
||||
String? remark,
|
||||
}) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final request = EquipmentHistoryRequestDto(
|
||||
equipmentsId: equipmentsId,
|
||||
warehousesId: warehousesId,
|
||||
transactionType: 'O', // 출고
|
||||
quantity: quantity,
|
||||
transactedAt: transactedAt ?? DateTime.now(),
|
||||
remark: remark,
|
||||
);
|
||||
|
||||
await _useCase.createEquipmentHistory(request);
|
||||
await loadHistory(refresh: true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// createHistory 메서드 (별칭)
|
||||
Future<bool> createHistory(EquipmentHistoryRequestDto request) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _useCase.createEquipmentHistory(request);
|
||||
await loadHistory(refresh: true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// searchEquipmentHistories 메서드
|
||||
Future<List<EquipmentHistoryDto>> searchEquipmentHistories({
|
||||
String? query,
|
||||
String? transactionType,
|
||||
int? equipmentId,
|
||||
int? warehouseId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
transactionType: transactionType,
|
||||
equipmentsId: equipmentId,
|
||||
warehousesId: warehouseId,
|
||||
);
|
||||
|
||||
List<EquipmentHistoryDto> histories = result.items;
|
||||
|
||||
// 간단한 텍스트 검색 (remark 필드 기반)
|
||||
if (query != null && query.isNotEmpty) {
|
||||
histories = histories.where((h) =>
|
||||
h.remark?.toLowerCase().contains(query.toLowerCase()) ?? false
|
||||
).toList();
|
||||
}
|
||||
|
||||
return histories;
|
||||
} catch (e) {
|
||||
_error = e.toString();
|
||||
notifyListeners();
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// setSearchQuery 메서드
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
setFilters(searchQuery: query);
|
||||
}
|
||||
|
||||
// loadInventoryStatus 메서드 (단순 재고 상태 로드)
|
||||
Future<void> loadInventoryStatus() async {
|
||||
await loadHistory(refresh: true);
|
||||
}
|
||||
|
||||
// loadWarehouseStock 메서드 (창고별 재고 로드)
|
||||
Future<void> loadWarehouseStock({int? warehouseId}) async {
|
||||
setFilters(warehouseId: warehouseId);
|
||||
}
|
||||
|
||||
// getAvailableStock 메서드
|
||||
Future<int> getAvailableStock(int equipmentId, {int? warehouseId}) async {
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
equipmentsId: equipmentId,
|
||||
warehousesId: warehouseId,
|
||||
);
|
||||
|
||||
int totalIn = result.items
|
||||
.where((h) => h.transactionType == 'I')
|
||||
.fold(0, (sum, h) => sum + h.quantity);
|
||||
int totalOut = result.items
|
||||
.where((h) => h.transactionType == 'O')
|
||||
.fold(0, (sum, h) => sum + h.quantity);
|
||||
|
||||
return totalIn - totalOut;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// getAvailableEquipments 메서드 (사용 가능한 장비 목록)
|
||||
Future<List<int>> getAvailableEquipments({int? warehouseId}) async {
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
warehousesId: warehouseId,
|
||||
);
|
||||
|
||||
// 재고가 있는 장비 ID들만 반환
|
||||
Map<int, int> stockMap = {};
|
||||
for (var history in result.items) {
|
||||
stockMap[history.equipmentsId] = (stockMap[history.equipmentsId] ?? 0) +
|
||||
(history.transactionType == 'I' ? history.quantity : -history.quantity);
|
||||
}
|
||||
|
||||
return stockMap.entries
|
||||
.where((entry) => entry.value > 0)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지네이션 메서드들
|
||||
Future<void> previousPage() async {
|
||||
if (_currentPage > 1) {
|
||||
_currentPage--;
|
||||
await loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> nextPage() async {
|
||||
if (_currentPage < totalPages) {
|
||||
_currentPage++;
|
||||
await loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 클리어
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_historyList.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,20 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/services/warehouse_service.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
import 'package:superport/core/utils/debug_logger.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
import 'package:superport/core/services/lookups_service.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_history_controller.dart';
|
||||
import 'package:superport/data/models/equipment/equipment_dto.dart';
|
||||
import 'package:superport/data/models/equipment_history_dto.dart';
|
||||
|
||||
/// 장비 입고 폼 컨트롤러
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentInFormController extends ChangeNotifier {
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
final WarehouseService _warehouseService = GetIt.instance<WarehouseService>();
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
// final WarehouseService _warehouseService = GetIt.instance<WarehouseService>(); // 사용되지 않음 - 제거
|
||||
// final CompanyService _companyService = GetIt.instance<CompanyService>(); // 사용되지 않음 - 제거
|
||||
final LookupsService _lookupsService = GetIt.instance<LookupsService>();
|
||||
final int? equipmentInId; // 실제로는 장비 ID (입고 ID가 아님)
|
||||
int? actualEquipmentId; // API 호출용 실제 장비 ID
|
||||
@@ -37,15 +36,15 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
/// canSave 상태 업데이트 (UI 렌더링 문제 해결)
|
||||
void _updateCanSave() {
|
||||
final hasEquipmentNumber = _equipmentNumber.trim().isNotEmpty;
|
||||
final hasManufacturer = _manufacturer.trim().isNotEmpty;
|
||||
final hasEquipmentNumber = _serialNumber.trim().isNotEmpty;
|
||||
final hasModelsId = _modelsId != null; // models_id 필수
|
||||
final isNotSaving = !_isSaving;
|
||||
|
||||
final newCanSave = isNotSaving && hasEquipmentNumber && hasManufacturer;
|
||||
final newCanSave = isNotSaving && hasEquipmentNumber && hasModelsId;
|
||||
|
||||
if (_canSave != newCanSave) {
|
||||
_canSave = newCanSave;
|
||||
print('🚀 [canSave 상태 변경] $_canSave → equipmentNumber: "$_equipmentNumber", manufacturer: "$_manufacturer"');
|
||||
print('🚀 [canSave 상태 변경] $_canSave → serialNumber: "$_serialNumber", modelsId: $_modelsId');
|
||||
notifyListeners(); // 명시적 UI 업데이트
|
||||
}
|
||||
}
|
||||
@@ -54,24 +53,18 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
// 입력 상태 변수 (백엔드 API 구조에 맞게 수정)
|
||||
String _equipmentNumber = ''; // 장비번호 (필수) - private으로 변경
|
||||
String _manufacturer = ''; // 제조사 (필수) - private으로 변경
|
||||
String _modelName = ''; // 모델명 - private으로 변경
|
||||
String _serialNumber = ''; // 시리얼번호 - private으로 변경
|
||||
String _category1 = ''; // 대분류 - private으로 변경
|
||||
String _category2 = ''; // 중분류 - private으로 변경
|
||||
String _category3 = ''; // 소분류 - private으로 변경
|
||||
String _serialNumber = ''; // 장비번호 (필수) - private으로 변경
|
||||
int? _modelsId; // 모델 ID (필수) - Vendor→Model cascade에서 선택
|
||||
int? _vendorId; // 벤더 ID (UI용, API에는 전송 안함)
|
||||
|
||||
// Legacy 필드 (UI 호환성 유지용)
|
||||
String _manufacturer = ''; // 제조사 (Legacy) - ModelDto에서 가져옴
|
||||
String _name = ''; // 모델명 (Legacy) - ModelDto에서 가져옴
|
||||
String _category1 = ''; // 대분류 (Legacy)
|
||||
String _category2 = ''; // 중분류 (Legacy)
|
||||
String _category3 = ''; // 소분류 (Legacy)
|
||||
|
||||
// Getters and Setters for reactive fields
|
||||
String get equipmentNumber => _equipmentNumber;
|
||||
set equipmentNumber(String value) {
|
||||
if (_equipmentNumber != value) {
|
||||
_equipmentNumber = value;
|
||||
_updateCanSave(); // canSave 상태 업데이트
|
||||
print('DEBUG [Controller] equipmentNumber updated: "$_equipmentNumber"');
|
||||
}
|
||||
}
|
||||
|
||||
String get serialNumber => _serialNumber;
|
||||
set serialNumber(String value) {
|
||||
if (_serialNumber != value) {
|
||||
@@ -90,12 +83,12 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String get modelName => _modelName;
|
||||
set modelName(String value) {
|
||||
if (_modelName != value) {
|
||||
_modelName = value;
|
||||
String get name => _name;
|
||||
set name(String value) {
|
||||
if (_name != value) {
|
||||
_name = value;
|
||||
_updateCanSave(); // canSave 상태 업데이트
|
||||
print('DEBUG [Controller] modelName updated: "$_modelName"');
|
||||
print('DEBUG [Controller] name updated: "$_name"');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +115,37 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
_updateCanSave(); // canSave 상태 업데이트
|
||||
}
|
||||
}
|
||||
|
||||
// 새로운 필드 getters/setters
|
||||
int? get modelsId => _modelsId;
|
||||
set modelsId(int? value) {
|
||||
if (_modelsId != value) {
|
||||
_modelsId = value;
|
||||
_updateCanSave(); // canSave 상태 업데이트
|
||||
print('DEBUG [Controller] modelsId updated: $_modelsId');
|
||||
}
|
||||
}
|
||||
|
||||
int? get vendorId => _vendorId;
|
||||
set vendorId(int? value) {
|
||||
if (_vendorId != value) {
|
||||
_vendorId = value;
|
||||
// vendor 변경 시 model 초기화
|
||||
if (_modelsId != null) {
|
||||
_modelsId = null;
|
||||
_updateCanSave();
|
||||
}
|
||||
print('DEBUG [Controller] vendorId updated: $_vendorId');
|
||||
}
|
||||
}
|
||||
|
||||
// Vendor→Model 선택 콜백
|
||||
void onVendorModelChanged(int? vendorId, int? modelId) {
|
||||
_vendorId = vendorId;
|
||||
_modelsId = modelId;
|
||||
_updateCanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
DateTime? purchaseDate; // 구매일
|
||||
double? purchasePrice; // 구매가격
|
||||
|
||||
@@ -139,6 +163,16 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
int? selectedCompanyId; // 구매처 ID
|
||||
int? selectedWarehouseId; // 입고지 ID
|
||||
|
||||
// 재고 초기화 필드
|
||||
int _initialStock = 1; // 초기 재고 수량 (기본값: 1)
|
||||
int get initialStock => _initialStock;
|
||||
set initialStock(int value) {
|
||||
if (_initialStock != value && value > 0) {
|
||||
_initialStock = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 창고 위치 전체 데이터 (이름-ID 매핑용)
|
||||
Map<String, int> warehouseLocationMap = {};
|
||||
|
||||
@@ -147,9 +181,9 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
// 수정불가 필드 목록 (수정 모드에서만 적용)
|
||||
static const List<String> _readOnlyFields = [
|
||||
'equipmentNumber', // 장비번호
|
||||
'serialNumber', // 장비번호
|
||||
'manufacturer', // 제조사
|
||||
'modelName', // 모델명
|
||||
'name', // 모델명
|
||||
'serialNumber', // 시리얼번호
|
||||
'purchaseDate', // 구매일
|
||||
'purchasePrice', // 구매가격
|
||||
@@ -259,45 +293,42 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
print('DEBUG [_loadEquipmentIn] Equipment loaded successfully');
|
||||
DebugLogger.log('장비 정보 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': equipment.id,
|
||||
'manufacturer': equipment.manufacturer,
|
||||
'equipmentNumber': equipment.equipmentNumber, // name → equipmentNumber
|
||||
'companiesId': equipment.companiesId, // 백엔드 실제 필드
|
||||
'serialNumber': equipment.serialNumber,
|
||||
});
|
||||
|
||||
// 장비 정보 설정 (새로운 필드 구조)
|
||||
// 장비 정보 설정 (백엔드 실제 필드 구조)
|
||||
print('DEBUG [_loadEquipmentIn] Setting equipment data...');
|
||||
print('DEBUG [_loadEquipmentIn] equipment.manufacturer="${equipment.manufacturer}"');
|
||||
print('DEBUG [_loadEquipmentIn] equipment.equipmentNumber="${equipment.equipmentNumber}"');
|
||||
print('DEBUG [_loadEquipmentIn] equipment.companiesId="${equipment.companiesId}"');
|
||||
print('DEBUG [_loadEquipmentIn] equipment.serialNumber="${equipment.serialNumber}"');
|
||||
|
||||
// 새로운 필드 구조로 매핑 (setter 사용하여 notifyListeners 자동 호출)
|
||||
_equipmentNumber = equipment.equipmentNumber ?? '';
|
||||
manufacturer = equipment.manufacturer ?? '';
|
||||
modelName = equipment.modelName ?? ''; // deprecated name 제거
|
||||
// 백엔드 실제 필드로 매핑
|
||||
_serialNumber = equipment.serialNumber ?? '';
|
||||
category1 = equipment.category1 ?? ''; // deprecated category 제거
|
||||
category2 = equipment.category2 ?? ''; // deprecated subCategory 제거
|
||||
category3 = equipment.category3 ?? ''; // deprecated subSubCategory 제거
|
||||
purchaseDate = equipment.purchaseDate;
|
||||
purchasePrice = equipment.purchasePrice;
|
||||
selectedCompanyId = equipment.companyId;
|
||||
selectedWarehouseId = equipment.warehouseLocationId;
|
||||
_modelsId = equipment.modelsId; // 백엔드 실제 필드
|
||||
selectedCompanyId = equipment.companiesId; // companyId → companiesId
|
||||
purchasePrice = equipment.purchasePrice.toDouble(); // int → double 변환
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
|
||||
print('DEBUG [_loadEquipmentIn] After setting - equipmentNumber="$_equipmentNumber", manufacturer="$_manufacturer", modelName="$_modelName"');
|
||||
// Legacy 필드들은 기본값으로 설정 (UI 호환성)
|
||||
manufacturer = ''; // 더 이상 백엔드에서 제공안함
|
||||
name = '';
|
||||
category1 = '';
|
||||
category2 = '';
|
||||
category3 = '';
|
||||
|
||||
print('DEBUG [_loadEquipmentIn] After setting - serialNumber="$_serialNumber", manufacturer="$_manufacturer", name="$_name"');
|
||||
// 🔧 [DEBUG] UI 업데이트를 위한 중요 필드들 로깅
|
||||
print('DEBUG [_loadEquipmentIn] purchaseDate: $purchaseDate, purchasePrice: $purchasePrice');
|
||||
print('DEBUG [_loadEquipmentIn] selectedCompanyId: $selectedCompanyId, selectedWarehouseId: $selectedWarehouseId');
|
||||
|
||||
DebugLogger.log('장비 데이터 설정 완료', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentNumber': _equipmentNumber,
|
||||
'manufacturer': _manufacturer,
|
||||
'modelName': _modelName,
|
||||
'category1': _category1,
|
||||
'category2': _category2,
|
||||
'category3': _category3,
|
||||
'serialNumber': _serialNumber,
|
||||
'companiesId': selectedCompanyId,
|
||||
'modelsId': _modelsId,
|
||||
'purchasePrice': purchasePrice,
|
||||
});
|
||||
|
||||
print('DEBUG [EQUIPMENT_IN]: Equipment loaded - equipmentNumber: "$_equipmentNumber", manufacturer: "$_manufacturer", modelName: "$_modelName"');
|
||||
print('DEBUG [EQUIPMENT_IN]: Equipment loaded - serialNumber: "$_serialNumber", companiesId: "$selectedCompanyId", modelsId: "$_modelsId"');
|
||||
|
||||
// 추가 필드들은 입고 시에는 필요하지 않으므로 생략
|
||||
|
||||
@@ -359,22 +390,18 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
if (_manufacturer.isNotEmpty && !manufacturers.contains(_manufacturer)) {
|
||||
manufacturers.add(_manufacturer);
|
||||
}
|
||||
if (_modelName.isNotEmpty && !equipmentNames.contains(_modelName)) {
|
||||
equipmentNames.add(_modelName);
|
||||
if (_name.isNotEmpty && !equipmentNames.contains(_name)) {
|
||||
equipmentNames.add(_name);
|
||||
}
|
||||
|
||||
// 백엔드 API 구조에 맞는 Equipment 객체 생성
|
||||
final equipment = Equipment(
|
||||
// Required 필드들
|
||||
manufacturer: _manufacturer.trim(),
|
||||
equipmentNumber: _equipmentNumber.trim(), // required
|
||||
modelName: _modelName.trim().isEmpty ? _equipmentNumber.trim() : _modelName.trim(), // required
|
||||
category1: _category1.trim().isEmpty ? 'N/A' : _category1.trim(), // required (nullable에서 non-nullable로)
|
||||
category2: _category2.trim().isEmpty ? 'N/A' : _category2.trim(), // required (nullable에서 non-nullable로)
|
||||
category3: _category3.trim().isEmpty ? 'N/A' : _category3.trim(), // required (nullable에서 non-nullable로)
|
||||
modelsId: _modelsId, // 필수: Model ID (Vendor→Model cascade에서 선택)
|
||||
equipmentNumber: _serialNumber.trim(), // required: 장비번호
|
||||
serialNumber: _serialNumber.trim(), // required: 시리얼번호
|
||||
quantity: 1, // required
|
||||
// Optional 필드들
|
||||
serialNumber: _serialNumber.trim().isEmpty ? null : _serialNumber.trim(),
|
||||
purchaseDate: purchaseDate,
|
||||
purchasePrice: purchasePrice,
|
||||
inDate: purchaseDate ?? DateTime.now(),
|
||||
@@ -393,13 +420,26 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
DebugLogger.log('장비 정보 업데이트 시작', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': actualEquipmentId,
|
||||
'equipmentNumber': equipment.equipmentNumber,
|
||||
'manufacturer': equipment.manufacturer,
|
||||
'modelName': equipment.modelName,
|
||||
'serialNumber': equipment.serialNumber,
|
||||
'modelsId': equipment.modelsId,
|
||||
'companyId': equipment.companyId,
|
||||
});
|
||||
|
||||
await _equipmentService.updateEquipment(actualEquipmentId!, equipment);
|
||||
// Equipment 객체를 EquipmentUpdateRequestDto로 변환
|
||||
final updateRequest = EquipmentUpdateRequestDto(
|
||||
companiesId: selectedCompanyId ?? 0,
|
||||
modelsId: _modelsId ?? 0,
|
||||
serialNumber: _serialNumber,
|
||||
barcode: null,
|
||||
purchasedAt: null,
|
||||
purchasePrice: purchasePrice?.toInt() ?? 0,
|
||||
warrantyNumber: '',
|
||||
warrantyStartedAt: DateTime.now(),
|
||||
warrantyEndedAt: DateTime.now().add(Duration(days: 365)),
|
||||
remark: remarkController.text.isNotEmpty ? remarkController.text : null,
|
||||
);
|
||||
|
||||
await _equipmentService.updateEquipment(actualEquipmentId!, updateRequest);
|
||||
|
||||
DebugLogger.log('장비 정보 업데이트 성공', tag: 'EQUIPMENT_IN');
|
||||
} else {
|
||||
@@ -407,25 +447,67 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
try {
|
||||
// 1. 먼저 장비 생성
|
||||
DebugLogger.log('장비 입고 시작', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentNumber': _equipmentNumber,
|
||||
'manufacturer': _manufacturer,
|
||||
'modelName': _modelName,
|
||||
'serialNumber': _serialNumber,
|
||||
'manufacturer': _manufacturer,
|
||||
'name': _name,
|
||||
'companiesId': selectedCompanyId,
|
||||
});
|
||||
|
||||
final createdEquipment = await _equipmentService.createEquipment(equipment);
|
||||
// Equipment 객체를 EquipmentRequestDto로 변환
|
||||
final createRequest = EquipmentRequestDto(
|
||||
companiesId: selectedCompanyId ?? 0,
|
||||
modelsId: _modelsId ?? 0,
|
||||
serialNumber: _serialNumber,
|
||||
barcode: null,
|
||||
purchasedAt: null,
|
||||
purchasePrice: purchasePrice?.toInt() ?? 0,
|
||||
warrantyNumber: '',
|
||||
warrantyStartedAt: DateTime.now(),
|
||||
warrantyEndedAt: DateTime.now().add(Duration(days: 365)),
|
||||
remark: remarkController.text.isNotEmpty ? remarkController.text : null,
|
||||
);
|
||||
|
||||
final createdEquipment = await _equipmentService.createEquipment(createRequest);
|
||||
|
||||
DebugLogger.log('장비 생성 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': createdEquipment.id,
|
||||
});
|
||||
|
||||
// 새로운 API에서는 장비 생성 시 입고 처리까지 한 번에 처리됨
|
||||
// 2. Equipment History (입고 기록) 생성
|
||||
if (selectedWarehouseId != null && createdEquipment.id != null) {
|
||||
try {
|
||||
// EquipmentHistoryController를 통한 입고 처리
|
||||
final historyController = EquipmentHistoryController();
|
||||
|
||||
// 입고 처리 (EquipmentHistoryRequestDto 객체 생성)
|
||||
final historyRequest = EquipmentHistoryRequestDto(
|
||||
equipmentsId: createdEquipment.id, // null 체크 이미 완료되어 ! 연산자 불필요
|
||||
warehousesId: selectedWarehouseId!,
|
||||
transactionType: 'I', // 입고: 'I'
|
||||
quantity: _initialStock,
|
||||
transactedAt: DateTime.now(),
|
||||
remark: '장비 등록 시 자동 입고',
|
||||
);
|
||||
|
||||
await historyController.createHistory(historyRequest);
|
||||
|
||||
DebugLogger.log('Equipment History 생성 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'equipmentId': createdEquipment.id,
|
||||
'warehouseId': selectedWarehouseId,
|
||||
'quantity': _initialStock,
|
||||
});
|
||||
} catch (e) {
|
||||
// 입고 실패 시에도 장비는 이미 생성되었으므로 경고만 표시
|
||||
DebugLogger.logError('Equipment History 생성 실패', error: e);
|
||||
_error = '장비는 등록되었으나 입고 처리 중 오류가 발생했습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
DebugLogger.log('입고 처리 성공', tag: 'EQUIPMENT_IN');
|
||||
DebugLogger.log('입고 처리 완료', tag: 'EQUIPMENT_IN');
|
||||
|
||||
} catch (e) {
|
||||
DebugLogger.logError('장비 입고 처리 실패', error: e);
|
||||
throw e; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
|
||||
rethrow; // 에러를 상위로 전파하여 적절한 에러 메시지 표시
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/core/utils/error_handler.dart';
|
||||
import 'package:superport/core/controllers/base_list_controller.dart';
|
||||
import 'package:superport/core/utils/equipment_status_converter.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/data/models/common/pagination_params.dart';
|
||||
import 'package:superport/core/services/lookups_service.dart';
|
||||
import 'package:superport/data/models/lookups/lookup_data.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/data/models/equipment/equipment_dto.dart';
|
||||
|
||||
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
@@ -76,8 +75,8 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
status: _statusFilter != null ?
|
||||
EquipmentStatusConverter.clientToServer(_statusFilter) : null,
|
||||
search: params.search,
|
||||
companyId: _companyIdFilter,
|
||||
includeInactive: _includeInactive,
|
||||
// companyId: _companyIdFilter, // 비활성화: EquipmentService에서 지원하지 않음
|
||||
// includeInactive: _includeInactive, // 비활성화: EquipmentService에서 지원하지 않음
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
@@ -102,18 +101,13 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
print('DEBUG [EquipmentListController] Converting ${apiEquipmentDtos.items.length} DTOs to UnifiedEquipment');
|
||||
final items = apiEquipmentDtos.items.map((dto) {
|
||||
// 🔧 [DEBUG] JOIN된 데이터 로깅
|
||||
print('DEBUG [EquipmentListController] DTO ID: ${dto.id}, companyName: "${dto.companyName}", warehouseName: "${dto.warehouseName}"');
|
||||
print('DEBUG [EquipmentListController] DTO ID: ${dto.id}, companyName: "${dto.companyName}"');
|
||||
final equipment = Equipment(
|
||||
id: dto.id,
|
||||
manufacturer: dto.manufacturer ?? 'Unknown',
|
||||
equipmentNumber: dto.equipmentNumber ?? 'Unknown', // name → equipmentNumber (required)
|
||||
modelName: dto.modelName ?? dto.equipmentNumber ?? 'Unknown', // 새로운 필수 필드 (required)
|
||||
// 🔧 [BUG FIX] 하드코딩 제거 - 백엔드 API에서 카테고리 정보 미제공 시 기본값 사용
|
||||
// TODO: 백엔드 API에서 category1/2/3 필드 추가 필요
|
||||
category1: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값
|
||||
category2: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값
|
||||
category3: 'N/A', // 백엔드에서 카테고리 정보 미제공 시 기본값
|
||||
serialNumber: dto.serialNumber,
|
||||
modelsId: dto.modelsId, // Sprint 3: Model FK 사용
|
||||
model: null, // Equipment 생성자에서 ModelDto? 요구, 현재 DTO는 modelName(String) 제공
|
||||
equipmentNumber: dto.serialNumber ?? 'Unknown', // 장비번호 (required)
|
||||
serialNumber: dto.serialNumber ?? 'Unknown', // 시리얼번호 (required)
|
||||
quantity: 1, // 기본 수량
|
||||
);
|
||||
|
||||
@@ -123,15 +117,15 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
final unifiedEquipment = UnifiedEquipment(
|
||||
id: dto.id,
|
||||
equipment: equipment,
|
||||
date: dto.createdAt ?? DateTime.now(),
|
||||
status: EquipmentStatusConverter.serverToClient(dto.status),
|
||||
notes: null, // EquipmentListDto에 remark 필드 없음
|
||||
date: dto.registeredAt ?? DateTime.now(), // EquipmentDto에는 createdAt 대신 registeredAt 존재
|
||||
status: '입고', // EquipmentDto에 status 필드 없음 - 기본값 설정 (실제는 Equipment_History에서 상태 관리)
|
||||
notes: dto.remark, // EquipmentDto에 remark 필드 존재
|
||||
// 🔧 [BUG FIX] 누락된 위치 정보 필드들 추가
|
||||
// 문제: 장비 리스트에서 위치 정보(현재 위치, 창고 위치)가 표시되지 않음
|
||||
// 원인: UnifiedEquipment 생성 시 JOIN된 데이터(companyName, warehouseName) 누락
|
||||
// 해결: EquipmentListDto의 JOIN된 데이터를 UnifiedEquipment 필드로 매핑
|
||||
// 원인: EquipmentDto에 warehouseName 필드가 없음 (백엔드 스키마에 warehouse 정보 분리)
|
||||
// 해결: 현재는 companyName만 사용, warehouseLocation은 null로 설정
|
||||
currentCompany: dto.companyName, // API company_name → currentCompany
|
||||
warehouseLocation: dto.warehouseName, // API warehouse_name → warehouseLocation
|
||||
warehouseLocation: null, // EquipmentDto에 warehouse_name 필드 없음
|
||||
// currentBranch는 EquipmentListDto에 없으므로 null (백엔드 API 구조 변경으로 지점 개념 제거)
|
||||
currentBranch: null,
|
||||
);
|
||||
@@ -156,10 +150,9 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
@override
|
||||
bool filterItem(UnifiedEquipment item, String query) {
|
||||
final q = query.toLowerCase();
|
||||
return (item.equipment.equipmentNumber.toLowerCase().contains(q)) || // name → equipmentNumber
|
||||
(item.equipment.modelName?.toLowerCase().contains(q) ?? false) || // 모델명 추가
|
||||
(item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.equipment.manufacturer.toLowerCase().contains(q)) ||
|
||||
return (item.equipment.serialNumber?.toLowerCase().contains(q) ?? false) || // serialNumber 검색
|
||||
(item.equipment.modelName.toLowerCase().contains(q)) || // Equipment.modelName getter 사용
|
||||
(item.equipment.manufacturer.toLowerCase().contains(q)) || // Equipment.manufacturer getter 사용
|
||||
(item.notes?.toLowerCase().contains(q) ?? false) ||
|
||||
(item.status.toLowerCase().contains(q));
|
||||
}
|
||||
@@ -277,8 +270,8 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _equipmentService.changeEquipmentStatus(
|
||||
id,
|
||||
EquipmentStatusConverter.clientToServer(newStatus),
|
||||
reason,
|
||||
EquipmentStatusConverter.clientToServer(newStatus),
|
||||
// reason 파라미터는 EquipmentService에서 지원하지 않음
|
||||
),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
@@ -314,7 +307,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
reason: reason ?? '폐기 처리',
|
||||
);
|
||||
} catch (e) {
|
||||
failedEquipments.add('${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}'); // name → equipmentNumber
|
||||
failedEquipments.add('${equipment.equipment.manufacturer} ${equipment.equipment.serialNumber}'); // name → serialNumber
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,8 +320,22 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
|
||||
/// 장비 정보 수정
|
||||
Future<void> updateEquipment(int id, UnifiedEquipment equipment) async {
|
||||
// Equipment → EquipmentUpdateRequestDto 변환 (실제 필드명으로 매핑)
|
||||
final updateRequest = EquipmentUpdateRequestDto(
|
||||
companiesId: equipment.equipment.currentCompanyId ?? equipment.equipment.companyId, // companiesId 필드 없음
|
||||
modelsId: equipment.equipment.modelsId, // ✓ 있음
|
||||
serialNumber: equipment.equipment.serialNumber, // ✓ 있음
|
||||
barcode: equipment.equipment.barcode, // ✓ 있음
|
||||
purchasedAt: equipment.equipment.purchaseDate, // purchasedAt → purchaseDate
|
||||
purchasePrice: equipment.equipment.purchasePrice?.toInt(), // double? → int? 변환
|
||||
warrantyNumber: equipment.equipment.warrantyLicense, // warrantyNumber → warrantyLicense
|
||||
warrantyStartedAt: equipment.equipment.warrantyStartDate, // warrantyStartedAt → warrantyStartDate
|
||||
warrantyEndedAt: equipment.equipment.warrantyEndDate, // warrantyEndedAt → warrantyEndDate
|
||||
remark: equipment.notes, // UnifiedEquipment.notes → DTO.remark
|
||||
);
|
||||
|
||||
await ErrorHandler.handleApiCall<void>(
|
||||
() => _equipmentService.updateEquipment(id, equipment.equipment),
|
||||
() => _equipmentService.updateEquipment(id, updateRequest),
|
||||
onError: (failure) {
|
||||
throw failure;
|
||||
},
|
||||
|
||||
@@ -4,16 +4,16 @@ import 'package:intl/intl.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_history_controller.dart';
|
||||
import 'package:superport/services/company_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/data/models/equipment_history_dto.dart';
|
||||
|
||||
/// 장비 출고 폼 컨트롤러
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentOutFormController extends ChangeNotifier {
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
// final EquipmentService _equipmentService = GetIt.instance<EquipmentService>(); // 사용되지 않음 - 제거
|
||||
final CompanyService _companyService = GetIt.instance<CompanyService>();
|
||||
int? equipmentOutId;
|
||||
|
||||
@@ -21,7 +21,7 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
bool isEditMode = false;
|
||||
|
||||
// 상태 관리
|
||||
bool _isLoading = false;
|
||||
final bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
// Getters
|
||||
@@ -221,26 +221,34 @@ class EquipmentOutFormController extends ChangeNotifier {
|
||||
} else {
|
||||
// 생성 모드
|
||||
if (selectedEquipments != null && selectedEquipments!.isNotEmpty) {
|
||||
// 다중 장비 출고
|
||||
// 다중 장비 출고 (새로운 EquipmentHistoryController 사용)
|
||||
final historyController = EquipmentHistoryController();
|
||||
|
||||
for (var equipmentData in selectedEquipments!) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
if (equipment.id != null) {
|
||||
await equipmentService.equipmentOut(
|
||||
equipmentId: equipment.id!,
|
||||
final historyRequest = EquipmentHistoryRequestDto(
|
||||
equipmentsId: equipment.id!,
|
||||
warehousesId: 1, // 기본 창고 ID (실제로는 사용자 선택 필요)
|
||||
transactionType: 'OUT',
|
||||
quantity: equipment.quantity,
|
||||
companyId: companyId,
|
||||
notes: note ?? remarkController.text,
|
||||
remark: note ?? remarkController.text,
|
||||
);
|
||||
await historyController.createHistory(historyRequest);
|
||||
}
|
||||
}
|
||||
} else if (selectedEquipment != null && selectedEquipment!.id != null) {
|
||||
// 단일 장비 출고
|
||||
await equipmentService.equipmentOut(
|
||||
equipmentId: selectedEquipment!.id!,
|
||||
// 단일 장비 출고 (새로운 EquipmentHistoryController 사용)
|
||||
final historyController = EquipmentHistoryController();
|
||||
|
||||
final historyRequest = EquipmentHistoryRequestDto(
|
||||
equipmentsId: selectedEquipment!.id!,
|
||||
warehousesId: 1, // 기본 창고 ID (실제로는 사용자 선택 필요)
|
||||
transactionType: 'OUT',
|
||||
quantity: selectedEquipment!.quantity,
|
||||
companyId: companyId,
|
||||
notes: note ?? remarkController.text,
|
||||
remark: note ?? remarkController.text,
|
||||
);
|
||||
await historyController.createHistory(historyRequest);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/screens/common/templates/form_layout_template.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/currency_formatter.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
import 'package:superport/core/widgets/category_cascade_form_field.dart';
|
||||
import 'controllers/equipment_in_form_controller.dart';
|
||||
import 'widgets/equipment_vendor_model_selector.dart';
|
||||
import 'package:superport/utils/formatters/number_formatter.dart';
|
||||
|
||||
/// 새로운 Equipment 입고 폼 (Lookup API 기반)
|
||||
class EquipmentInFormScreen extends StatefulWidget {
|
||||
@@ -21,6 +20,9 @@ class EquipmentInFormScreen extends StatefulWidget {
|
||||
|
||||
class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
late EquipmentInFormController _controller;
|
||||
late TextEditingController _serialNumberController;
|
||||
late TextEditingController _initialStockController;
|
||||
late TextEditingController _purchasePriceController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -28,10 +30,26 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
_controller = EquipmentInFormController(equipmentInId: widget.equipmentInId);
|
||||
_controller.addListener(_onControllerUpdated);
|
||||
|
||||
// TextEditingController 초기화
|
||||
_serialNumberController = TextEditingController(text: _controller.serialNumber);
|
||||
_serialNumberController = TextEditingController(text: _controller.serialNumber);
|
||||
_initialStockController = TextEditingController(text: _controller.initialStock.toString());
|
||||
_purchasePriceController = TextEditingController(
|
||||
text: _controller.purchasePrice != null
|
||||
? CurrencyFormatter.formatKRW(_controller.purchasePrice)
|
||||
: ''
|
||||
);
|
||||
|
||||
// 수정 모드일 때 데이터 로드
|
||||
if (_controller.isEditMode) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await _controller.initializeForEdit();
|
||||
// 데이터 로드 후 컨트롤러 업데이트
|
||||
_serialNumberController.text = _controller.serialNumber;
|
||||
_serialNumberController.text = _controller.serialNumber;
|
||||
_purchasePriceController.text = _controller.purchasePrice != null
|
||||
? CurrencyFormatter.formatKRW(_controller.purchasePrice)
|
||||
: '';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,6 +58,10 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
void dispose() {
|
||||
_controller.removeListener(_onControllerUpdated);
|
||||
_controller.dispose();
|
||||
_serialNumberController.dispose();
|
||||
_serialNumberController.dispose();
|
||||
_initialStockController.dispose();
|
||||
_purchasePriceController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -47,25 +69,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
// 유효한 제조사 값 반환 (드롭다운 assertion 오류 방지)
|
||||
String? _getValidManufacturer() {
|
||||
if (_controller.manufacturer.isEmpty) return null;
|
||||
|
||||
final isValid = _controller.manufacturers.contains(_controller.manufacturer);
|
||||
print('DEBUG [_getValidManufacturer] manufacturer: "${_controller.manufacturer}", isValid: $isValid, available: ${_controller.manufacturers.take(5).toList()}');
|
||||
|
||||
return isValid ? _controller.manufacturer : null;
|
||||
}
|
||||
|
||||
// 유효한 모델명 값 반환 (드롭다운 assertion 오류 방지)
|
||||
String? _getValidModelName() {
|
||||
if (_controller.modelName.isEmpty) return null;
|
||||
|
||||
final isValid = _controller.equipmentNames.contains(_controller.modelName);
|
||||
print('DEBUG [_getValidModelName] modelName: "${_controller.modelName}", isValid: $isValid, available: ${_controller.equipmentNames.take(5).toList()}');
|
||||
|
||||
return isValid ? _controller.modelName : null;
|
||||
}
|
||||
// Legacy 필드 제거 - Vendor/Model cascade selector 사용
|
||||
|
||||
// 유효한 구매처 ID 반환 (드롭다운 assertion 오류 방지)
|
||||
int? _getValidCompanyId() {
|
||||
@@ -94,10 +98,10 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
if (success && mounted) {
|
||||
Navigator.pop(context, true);
|
||||
} else if (_controller.error != null && mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_controller.error!),
|
||||
backgroundColor: Colors.red,
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('오류'),
|
||||
description: Text(_controller.error!),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -106,7 +110,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 간소화된 디버깅
|
||||
print('🎯 [UI] canSave: ${_controller.canSave} | 장비번호: "${_controller.equipmentNumber}" | 제조사: "${_controller.manufacturer}"');
|
||||
print('🎯 [UI] canSave: ${_controller.canSave} | 장비번호: "${_controller.serialNumber}" | 제조사: "${_controller.manufacturer}"');
|
||||
|
||||
return FormLayoutTemplate(
|
||||
title: _controller.isEditMode ? '장비 수정' : '장비 입고',
|
||||
@@ -114,7 +118,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
isLoading: _controller.isSaving,
|
||||
child: _controller.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
? const Center(child: ShadProgress())
|
||||
: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
@@ -138,7 +142,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
}
|
||||
|
||||
Widget _buildBasicFields() {
|
||||
return Card(
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -153,142 +157,50 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 장비 번호 (필수)
|
||||
TextFormField(
|
||||
initialValue: _controller.equipmentNumber,
|
||||
readOnly: _controller.isFieldReadOnly('equipmentNumber'),
|
||||
decoration: InputDecoration(
|
||||
labelText: _controller.isFieldReadOnly('equipmentNumber')
|
||||
? '장비 번호 * 🔒' : '장비 번호 *',
|
||||
// 🔧 [UI FIX] ReadOnly 필드에서도 의미 있는 hintText 표시
|
||||
hintText: _controller.isFieldReadOnly('equipmentNumber')
|
||||
? (_controller.equipmentNumber.isNotEmpty ? null : '장비 번호 없음')
|
||||
: '장비 번호를 입력하세요',
|
||||
border: const OutlineInputBorder(),
|
||||
filled: _controller.isFieldReadOnly('equipmentNumber'),
|
||||
fillColor: _controller.isFieldReadOnly('equipmentNumber')
|
||||
? Colors.grey[100] : null,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('equipmentNumber')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
ShadInputFormField(
|
||||
controller: _serialNumberController,
|
||||
readOnly: _controller.isFieldReadOnly('serialNumber'),
|
||||
placeholder: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? (_controller.serialNumber.isNotEmpty ? _controller.serialNumber : '장비 번호 없음')
|
||||
: '장비 번호를 입력하세요'),
|
||||
label: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '장비 번호 * 🔒' : '장비 번호 *'),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
if (value.trim().isEmpty ?? true) {
|
||||
return '장비 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: _controller.isFieldReadOnly('equipmentNumber') ? null : (value) {
|
||||
_controller.equipmentNumber = value?.trim() ?? '';
|
||||
onChanged: _controller.isFieldReadOnly('serialNumber') ? null : (value) {
|
||||
_controller.serialNumber = value.trim() ?? '';
|
||||
setState(() {});
|
||||
print('DEBUG [장비번호 입력] value: "$value", controller.equipmentNumber: "${_controller.equipmentNumber}"');
|
||||
},
|
||||
onSaved: (value) {
|
||||
_controller.equipmentNumber = value?.trim() ?? '';
|
||||
print('DEBUG [장비번호 입력] value: "$value", controller.serialNumber: "${_controller.serialNumber}"');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 제조사 (필수, Dropdown)
|
||||
DropdownButtonFormField<String>(
|
||||
value: _getValidManufacturer(),
|
||||
items: _controller.manufacturers.map((String manufacturer) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: manufacturer,
|
||||
child: Text(
|
||||
manufacturer,
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('manufacturer')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
decoration: InputDecoration(
|
||||
labelText: _controller.isFieldReadOnly('manufacturer')
|
||||
? '제조사 * 🔒' : '제조사 *',
|
||||
hintText: _controller.isFieldReadOnly('manufacturer')
|
||||
? '수정불가' : '제조사를 선택하세요',
|
||||
border: const OutlineInputBorder(),
|
||||
filled: _controller.isFieldReadOnly('manufacturer'),
|
||||
fillColor: _controller.isFieldReadOnly('manufacturer')
|
||||
? Colors.grey[100] : null,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return '제조사는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: _controller.isFieldReadOnly('manufacturer') ? null : (value) {
|
||||
setState(() {
|
||||
_controller.manufacturer = value?.trim() ?? '';
|
||||
});
|
||||
print('🔧 DEBUG [제조사 선택] value: "$value", controller.manufacturer: "${_controller.manufacturer}", canSave: ${_controller.canSave}');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 모델명 (선택, Dropdown)
|
||||
DropdownButtonFormField<String>(
|
||||
value: _getValidModelName(),
|
||||
items: _controller.equipmentNames.map((String equipmentName) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: equipmentName,
|
||||
child: Text(
|
||||
equipmentName,
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('modelName')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
decoration: InputDecoration(
|
||||
labelText: _controller.isFieldReadOnly('modelName')
|
||||
? '모델명 🔒' : '모델명',
|
||||
hintText: _controller.isFieldReadOnly('modelName')
|
||||
? '수정불가' : '모델명을 선택하세요',
|
||||
border: const OutlineInputBorder(),
|
||||
filled: _controller.isFieldReadOnly('modelName'),
|
||||
fillColor: _controller.isFieldReadOnly('modelName')
|
||||
? Colors.grey[100] : null,
|
||||
),
|
||||
onChanged: _controller.isFieldReadOnly('modelName') ? null : (value) {
|
||||
setState(() {
|
||||
_controller.modelName = value?.trim() ?? '';
|
||||
});
|
||||
print('DEBUG [모델명 선택] value: "$value", controller.modelName: "${_controller.modelName}"');
|
||||
},
|
||||
// Vendor→Model cascade 선택기
|
||||
EquipmentVendorModelSelector(
|
||||
initialVendorId: _controller.vendorId,
|
||||
initialModelId: _controller.modelsId,
|
||||
onChanged: _controller.onVendorModelChanged,
|
||||
isReadOnly: _controller.isFieldReadOnly('modelsId'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 시리얼 번호 (선택)
|
||||
TextFormField(
|
||||
initialValue: _controller.serialNumber,
|
||||
ShadInputFormField(
|
||||
controller: _serialNumberController,
|
||||
readOnly: _controller.isFieldReadOnly('serialNumber'),
|
||||
decoration: InputDecoration(
|
||||
labelText: _controller.isFieldReadOnly('serialNumber')
|
||||
? '시리얼 번호 🔒' : '시리얼 번호',
|
||||
hintText: _controller.isFieldReadOnly('serialNumber')
|
||||
? '수정불가' : '시리얼 번호를 입력하세요',
|
||||
border: const OutlineInputBorder(),
|
||||
filled: _controller.isFieldReadOnly('serialNumber'),
|
||||
fillColor: _controller.isFieldReadOnly('serialNumber')
|
||||
? Colors.grey[100] : null,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('serialNumber')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
placeholder: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '수정불가' : '시리얼 번호를 입력하세요'),
|
||||
label: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '시리얼 번호 🔒' : '시리얼 번호'),
|
||||
onChanged: _controller.isFieldReadOnly('serialNumber') ? null : (value) {
|
||||
_controller.serialNumber = value?.trim() ?? '';
|
||||
_controller.serialNumber = value.trim() ?? '';
|
||||
setState(() {});
|
||||
print('DEBUG [시리얼번호 입력] value: "$value", controller.serialNumber: "${_controller.serialNumber}"');
|
||||
},
|
||||
onSaved: (value) {
|
||||
_controller.serialNumber = value?.trim() ?? '';
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -297,7 +209,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
}
|
||||
|
||||
Widget _buildCategorySection() {
|
||||
return Card(
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -328,7 +240,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
}
|
||||
|
||||
Widget _buildLocationSection() {
|
||||
return Card(
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -343,55 +255,73 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 구매처 (드롭다운 전용)
|
||||
DropdownButtonFormField<int>(
|
||||
value: _getValidCompanyId(),
|
||||
items: _controller.companies.entries.map((entry) {
|
||||
return DropdownMenuItem<int>(
|
||||
ShadSelect<int>(
|
||||
initialValue: _getValidCompanyId(),
|
||||
placeholder: const Text('구매처를 선택하세요'),
|
||||
options: _controller.companies.entries.map((entry) =>
|
||||
ShadOption(
|
||||
value: entry.key,
|
||||
child: Text(entry.value),
|
||||
);
|
||||
}).toList(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: '구매처',
|
||||
hintText: '구매처를 선택하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
)
|
||||
).toList(),
|
||||
selectedOptionBuilder: (context, value) =>
|
||||
Text(_controller.companies[value] ?? '선택하세요'),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedCompanyId = value;
|
||||
});
|
||||
print('DEBUG [구매처 선택] value: $value, companies: ${_controller.companies.length}');
|
||||
},
|
||||
onSaved: (value) {
|
||||
_controller.selectedCompanyId = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 입고지 (드롭다운 전용)
|
||||
DropdownButtonFormField<int>(
|
||||
value: _getValidWarehouseId(),
|
||||
items: _controller.warehouses.entries.map((entry) {
|
||||
return DropdownMenuItem<int>(
|
||||
ShadSelect<int>(
|
||||
initialValue: _getValidWarehouseId(),
|
||||
placeholder: const Text('입고지를 선택하세요'),
|
||||
options: _controller.warehouses.entries.map((entry) =>
|
||||
ShadOption(
|
||||
value: entry.key,
|
||||
child: Text(entry.value),
|
||||
);
|
||||
}).toList(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: '입고지',
|
||||
hintText: '입고지를 선택하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
)
|
||||
).toList(),
|
||||
selectedOptionBuilder: (context, value) =>
|
||||
Text(_controller.warehouses[value] ?? '선택하세요'),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedWarehouseId = value;
|
||||
});
|
||||
print('DEBUG [입고지 선택] value: $value, warehouses: ${_controller.warehouses.length}');
|
||||
},
|
||||
onSaved: (value) {
|
||||
_controller.selectedWarehouseId = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 초기 재고 수량 (신규 등록 시에만 표시)
|
||||
if (!_controller.isEditMode)
|
||||
ShadInputFormField(
|
||||
controller: _initialStockController,
|
||||
label: const Text('초기 재고 수량 *'),
|
||||
placeholder: const Text('입고할 수량을 입력하세요'),
|
||||
description: const Text('장비 등록 시 자동으로 입고 처리됩니다'),
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return '재고 수량은 필수입니다';
|
||||
}
|
||||
final quantity = int.tryParse(value);
|
||||
if (quantity == null || quantity <= 0) {
|
||||
return '1개 이상의 수량을 입력하세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (value) {
|
||||
final quantity = int.tryParse(value) ?? 1;
|
||||
_controller.initialStock = quantity;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -399,7 +329,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
}
|
||||
|
||||
Widget _buildPurchaseSection() {
|
||||
return Card(
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -431,29 +361,39 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
});
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: _controller.isFieldReadOnly('purchaseDate')
|
||||
? '구매일 🔒' : '구매일',
|
||||
suffixIcon: Icon(
|
||||
Icons.calendar_today,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[600] : null,
|
||||
? Colors.grey[300]!
|
||||
: Theme.of(context).dividerColor,
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
filled: _controller.isFieldReadOnly('purchaseDate'),
|
||||
fillColor: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[100] : null,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[50]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
// 🔧 [UI FIX] ReadOnly 필드에서도 의미 있는 텍스트 표시
|
||||
_controller.purchaseDate != null
|
||||
? '${_controller.purchaseDate!.year}-${_controller.purchaseDate!.month.toString().padLeft(2, '0')}-${_controller.purchaseDate!.day.toString().padLeft(2, '0')}'
|
||||
: _controller.isFieldReadOnly('purchaseDate') ? '구매일 미설정' : '날짜 선택',
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_controller.purchaseDate != null
|
||||
? '${_controller.purchaseDate!.year}-${_controller.purchaseDate!.month.toString().padLeft(2, '0')}-${_controller.purchaseDate!.day.toString().padLeft(2, '0')}'
|
||||
: _controller.isFieldReadOnly('purchaseDate') ? '구매일 미설정' : '날짜 선택',
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[600]
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -462,31 +402,21 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
|
||||
// 구매 가격
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _controller.purchasePrice != null
|
||||
? CurrencyFormatter.formatKRW(_controller.purchasePrice)
|
||||
: '',
|
||||
child: ShadInputFormField(
|
||||
controller: _purchasePriceController,
|
||||
readOnly: _controller.isFieldReadOnly('purchasePrice'),
|
||||
decoration: InputDecoration(
|
||||
labelText: _controller.isFieldReadOnly('purchasePrice')
|
||||
? '구매 가격 🔒' : '구매 가격',
|
||||
hintText: _controller.isFieldReadOnly('purchasePrice')
|
||||
? '수정불가' : '₩2,000,000',
|
||||
border: const OutlineInputBorder(),
|
||||
filled: _controller.isFieldReadOnly('purchasePrice'),
|
||||
fillColor: _controller.isFieldReadOnly('purchasePrice')
|
||||
? Colors.grey[100] : null,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('purchasePrice')
|
||||
? Colors.grey[600] : null,
|
||||
),
|
||||
label: Text(_controller.isFieldReadOnly('purchasePrice')
|
||||
? '구매 가격 🔒' : '구매 가격'),
|
||||
placeholder: Text(_controller.isFieldReadOnly('purchasePrice')
|
||||
? '수정불가' : '₩2,000,000'),
|
||||
keyboardType: _controller.isFieldReadOnly('purchasePrice')
|
||||
? null : TextInputType.number,
|
||||
inputFormatters: _controller.isFieldReadOnly('purchasePrice')
|
||||
? null : [KRWTextInputFormatter()],
|
||||
onSaved: (value) {
|
||||
_controller.purchasePrice = CurrencyFormatter.parseKRW(value);
|
||||
? null : [CurrencyInputFormatter()], // 새로운 통화 포맷터
|
||||
onChanged: (value) {
|
||||
// 숫자만 추출하여 저장
|
||||
final digitsOnly = value.replaceAll(RegExp(r'[^\d]'), '');
|
||||
_controller.purchasePrice = int.tryParse(digitsOnly)?.toDouble();
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -499,7 +429,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
}
|
||||
|
||||
Widget _buildRemarkSection() {
|
||||
return Card(
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -513,13 +443,10 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextFormField(
|
||||
ShadInputFormField(
|
||||
controller: _controller.remarkController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '비고',
|
||||
hintText: '비고사항을 입력하세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
label: const Text('비고'),
|
||||
placeholder: const Text('비고사항을 입력하세요'),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/common/components/shadcn_components.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
@@ -9,15 +10,13 @@ import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/utils/equipment_display_helper.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 장비 관리 화면
|
||||
class EquipmentList extends StatefulWidget {
|
||||
final String currentRoute;
|
||||
|
||||
const EquipmentList({Key? key, this.currentRoute = Routes.equipment})
|
||||
: super(key: key);
|
||||
const EquipmentList({super.key, this.currentRoute = Routes.equipment});
|
||||
|
||||
@override
|
||||
State<EquipmentList> createState() => _EquipmentListState();
|
||||
@@ -28,7 +27,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
bool _showDetailedColumns = true;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _horizontalScrollController = ScrollController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
String _selectedStatus = 'all';
|
||||
// String _searchKeyword = ''; // Removed - unused field
|
||||
String _appliedSearchKeyword = '';
|
||||
@@ -92,10 +90,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
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 {
|
||||
@@ -144,25 +138,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
_controller.updateSearchKeyword(_searchController.text);
|
||||
}
|
||||
|
||||
/// 장비 선택/해제
|
||||
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
||||
if (id == null) return;
|
||||
|
||||
// UnifiedEquipment를 찾아서 선택/해제
|
||||
UnifiedEquipment? equipment;
|
||||
try {
|
||||
equipment = _controller.items.firstWhere(
|
||||
(e) => e.equipment.id == id && e.status == status,
|
||||
);
|
||||
} catch (e) {
|
||||
// 해당하는 장비를 찾지 못함
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_controller.selectEquipment(equipment!);
|
||||
});
|
||||
}
|
||||
|
||||
/// 전체 선택/해제
|
||||
void _onSelectAll(bool? value) {
|
||||
@@ -194,18 +169,13 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
equipments = equipments.where((e) {
|
||||
final keyword = _appliedSearchKeyword.toLowerCase();
|
||||
return [
|
||||
e.equipment.manufacturer,
|
||||
e.equipment.equipmentNumber, // name → equipmentNumber (메인 필드)
|
||||
e.equipment.modelName ?? '', // 모델명 추가
|
||||
e.equipment.category1, // category → category1 (메인 필드)
|
||||
e.equipment.category2, // subCategory → category2 (메인 필드)
|
||||
e.equipment.category3, // subSubCategory → category3 (메인 필드)
|
||||
e.equipment.serialNumber ?? '',
|
||||
e.equipment.barcode ?? '',
|
||||
e.equipment.remark ?? '',
|
||||
e.equipment.warrantyLicense ?? '',
|
||||
e.notes ?? '',
|
||||
].any((field) => field.toLowerCase().contains(keyword));
|
||||
e.equipment.model?.vendor?.name ?? '', // Vendor 이름
|
||||
e.equipment.serialNumber ?? '', // 시리얼 번호 (메인 필드)
|
||||
e.equipment.model?.name ?? '', // Model 이름
|
||||
e.equipment.serialNumber ?? '', // 시리얼 번호 (중복 제거)
|
||||
e.equipment.barcode ?? '', // 바코드
|
||||
e.equipment.remark ?? '', // 비고
|
||||
].any((field) => field.toLowerCase().contains(keyword.toLowerCase()));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -215,8 +185,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
/// 출고 처리 버튼 핸들러
|
||||
void _handleOutEquipment() async {
|
||||
if (_controller.getSelectedInStockCount() == 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('출고할 장비를 선택해주세요.')),
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('출고할 장비를 선택해주세요.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -241,16 +214,20 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
/// 대여 처리 버튼 핸들러
|
||||
void _handleRentEquipment() async {
|
||||
if (_controller.getSelectedInStockCount() == 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('대여할 장비를 선택해주세요.')),
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('대여할 장비를 선택해주세요.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedEquipmentsSummary = _controller.getSelectedEquipmentsSummary();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${selectedEquipmentsSummary.length}개 장비 대여 기능은 준비 중입니다.'),
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('알림'),
|
||||
description: Text('${selectedEquipmentsSummary.length}개 장비 대여 기능은 준비 중입니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -262,8 +239,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
.toList();
|
||||
|
||||
if (selectedEquipments.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('폐기할 장비를 선택해주세요. (이미 폐기된 장비는 제외)')),
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('폐기할 장비를 선택해주세요. (이미 폐기된 장비는 제외)'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -271,11 +251,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
// 폐기 사유 입력을 위한 컨트롤러
|
||||
final TextEditingController reasonController = TextEditingController();
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
final result = await showShadDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (context) => ShadDialog(
|
||||
title: const Text('폐기 확인'),
|
||||
content: SingleChildScrollView(
|
||||
description: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -289,7 +269,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
'${equipment.manufacturer} ${equipment.equipmentNumber}', // name → equipmentNumber
|
||||
'${equipment.model?.vendor?.name ?? 'N/A'} ${equipment.serialNumber}', // Vendor + Equipment Number
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
);
|
||||
@@ -297,25 +277,22 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
const SizedBox(height: 16),
|
||||
const Text('폐기 사유:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
ShadInputFormField(
|
||||
controller: reasonController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '폐기 사유를 입력해주세요',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
placeholder: const Text('폐기 사유를 입력해주세요'),
|
||||
maxLines: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
ShadButton.destructive(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('폐기', style: TextStyle(color: Colors.red)),
|
||||
child: const Text('폐기'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -323,11 +300,13 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
if (result == true) {
|
||||
// 로딩 다이얼로그 표시
|
||||
showDialog(
|
||||
showShadDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
builder: (context) => const ShadDialog(
|
||||
child: Center(
|
||||
child: ShadProgress(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -338,8 +317,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // 로딩 다이얼로그 닫기
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('선택한 장비가 폐기 처리되었습니다.')),
|
||||
ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('폐기 완료'),
|
||||
description: Text('선택한 장비가 폐기 처리되었습니다.'),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_controller.loadData(isRefresh: true);
|
||||
@@ -348,8 +330,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context); // 로딩 다이얼로그 닫기
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('폐기 처리 실패: ${e.toString()}')),
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('폐기 실패'),
|
||||
description: Text(e.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -382,17 +367,17 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
/// 삭제 핸들러
|
||||
void _handleDelete(UnifiedEquipment equipment) {
|
||||
showDialog(
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (context) => ShadDialog(
|
||||
title: const Text('삭제 확인'),
|
||||
content: const Text('이 장비 정보를 삭제하시겠습니까?'),
|
||||
description: const Text('이 장비 정보를 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
ShadButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
@@ -400,48 +385,31 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
await _controller.deleteEquipment(equipment.equipment.id!, equipment.status);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비가 삭제되었습니다.')),
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('장비 삭제'),
|
||||
description: const Text('장비가 삭제되었습니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('삭제 실패: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('삭제 실패'),
|
||||
description: Text(e.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('삭제', style: TextStyle(color: Colors.red)),
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 이력 보기 핸들러
|
||||
void _handleHistory(UnifiedEquipment equipment) async {
|
||||
if (equipment.equipment.id == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('장비 ID가 없습니다.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 팝업 다이얼로그로 이력 표시
|
||||
final result = await EquipmentHistoryDialog.show(
|
||||
context: context,
|
||||
equipmentId: equipment.equipment.id!,
|
||||
equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}', // name → equipmentNumber
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
_controller.loadData(isRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -511,7 +479,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
controller: _searchController,
|
||||
onSubmitted: (_) => _onSearch(),
|
||||
decoration: InputDecoration(
|
||||
hintText: '장비명, 제조사, 카테고리, 시리얼번호 등...',
|
||||
hintText: '제조사, 모델명, 시리얼번호, 바코드 등...',
|
||||
hintStyle: TextStyle(color: ShadcnTheme.mutedForeground.withValues(alpha: 0.8), fontSize: 14),
|
||||
prefixIcon: Icon(Icons.search, color: ShadcnTheme.muted, size: 20),
|
||||
border: InputBorder.none,
|
||||
@@ -539,22 +507,21 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 상태 필터 드롭다운 (캐시된 데이터 사용)
|
||||
Container(
|
||||
SizedBox(
|
||||
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: _buildStatusDropdownItems(),
|
||||
width: 150,
|
||||
child: ShadSelect<String>(
|
||||
selectedOptionBuilder: (context, value) => Text(
|
||||
_getStatusDisplayText(value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
placeholder: const Text('상태 선택'),
|
||||
options: _buildStatusSelectOptions(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
_onStatusFilterChanged(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -573,12 +540,13 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
// TODO: 실제 권한 체크 로직 추가 필요
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
ShadCheckbox(
|
||||
value: _controller.includeInactive,
|
||||
onChanged: (_) => setState(() {
|
||||
_controller.toggleIncludeInactive();
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('비활성 포함'),
|
||||
],
|
||||
),
|
||||
@@ -635,8 +603,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
ShadcnButton(
|
||||
text: '재입고',
|
||||
onPressed: selectedOutCount > 0
|
||||
? () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('재입고 기능은 준비 중입니다.')),
|
||||
? () => ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('재입고 기능은 준비 중입니다.'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
variant: selectedOutCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
@@ -646,8 +617,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
ShadcnButton(
|
||||
text: '수리 요청',
|
||||
onPressed: selectedOutCount > 0
|
||||
? () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('수리 요청 기능은 준비 중입니다.')),
|
||||
? () => ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('수리 요청 기능은 준비 중입니다.'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
variant: selectedOutCount > 0 ? ShadcnButtonVariant.destructive : ShadcnButtonVariant.secondary,
|
||||
@@ -661,8 +635,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
ShadcnButton(
|
||||
text: '반납',
|
||||
onPressed: selectedRentCount > 0
|
||||
? () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('대여 반납 기능은 준비 중입니다.')),
|
||||
? () => ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('대여 반납 기능은 준비 중입니다.'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
@@ -672,8 +649,11 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
ShadcnButton(
|
||||
text: '연장',
|
||||
onPressed: selectedRentCount > 0
|
||||
? () => ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('대여 연장 기능은 준비 중입니다.')),
|
||||
? () => ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
title: Text('알림'),
|
||||
description: Text('대여 연장 기능은 준비 중입니다.'),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
@@ -795,180 +775,212 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 유연한 테이블 빌더
|
||||
/// 유연한 테이블 빌더 - Virtual Scrolling 적용
|
||||
Widget _buildFlexibleTable(List<UnifiedEquipment> pagedEquipments, {required bool useExpanded}) {
|
||||
final hasOutOrRent = pagedEquipments.any((e) =>
|
||||
e.status == EquipmentStatus.out || e.status == EquipmentStatus.rent
|
||||
);
|
||||
|
||||
// 헤더를 별도로 빌드
|
||||
Widget header = Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
value: _isAllSelected(),
|
||||
onChanged: (bool? value) => _onSelectAll(value),
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 40,
|
||||
),
|
||||
// 번호
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 50),
|
||||
// 제조사
|
||||
_buildHeaderCell('제조사', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 장비번호
|
||||
_buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 모델명
|
||||
_buildHeaderCell('모델명', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 상세 정보 (조건부) - 바코드로 변경
|
||||
if (_showDetailedColumns) ...[
|
||||
_buildHeaderCell('바코드', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
],
|
||||
// 수량
|
||||
_buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 50),
|
||||
// 재고 상태
|
||||
_buildHeaderCell('재고', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
// 상태
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 입출고일
|
||||
_buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 빈 상태 처리
|
||||
if (pagedEquipments.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
header,
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'데이터가 없습니다',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Virtual Scrolling을 위한 CustomScrollView 사용
|
||||
return Column(
|
||||
children: [
|
||||
// 테이블 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
Checkbox(
|
||||
value: _isAllSelected(),
|
||||
onChanged: _onSelectAll,
|
||||
header, // 헤더는 고정
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: ScrollController(),
|
||||
itemCount: pagedEquipments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final UnifiedEquipment equipment = pagedEquipments[index];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: 4,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 40,
|
||||
),
|
||||
// 번호
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 50),
|
||||
// 제조사
|
||||
_buildHeaderCell('제조사', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 장비번호
|
||||
_buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 모델명
|
||||
_buildHeaderCell('모델명', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 상세 정보 (조건부)
|
||||
if (_showDetailedColumns) ...[
|
||||
_buildHeaderCell('시리얼번호', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
],
|
||||
// 수량
|
||||
_buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 50),
|
||||
// 상태
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 입출고일
|
||||
_buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90),
|
||||
],
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.black),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
value: _selectedItems.contains(equipment.equipment.id ?? 0),
|
||||
onChanged: (bool? value) {
|
||||
if (equipment.equipment.id != null) {
|
||||
_onItemSelected(equipment.equipment.id!, value ?? false);
|
||||
}
|
||||
},
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 40,
|
||||
),
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 제조사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.model?.vendor?.name ?? 'N/A',
|
||||
equipment.equipment.model?.vendor?.name ?? 'N/A',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 장비번호
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.serialNumber ?? '',
|
||||
equipment.equipment.serialNumber ?? '',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 모델명
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.model?.name ?? '-',
|
||||
equipment.equipment.model?.name ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 상세 정보 (조건부) - 바코드로 변경
|
||||
if (_showDetailedColumns) ...[
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.barcode ?? '-',
|
||||
equipment.equipment.barcode ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
],
|
||||
// 수량 (백엔드에서 관리하지 않으므로 고정값)
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'1',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 재고 상태
|
||||
_buildDataCell(
|
||||
_buildInventoryStatus(equipment),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 상태
|
||||
_buildDataCell(
|
||||
_buildStatusBadge(equipment.status),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 입출고일
|
||||
_buildDataCell(
|
||||
_buildCreatedDateWidget(equipment),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 관리
|
||||
_buildDataCell(
|
||||
_buildActionButtons(equipment.equipment.id ?? 0),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 90,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 테이블 데이터
|
||||
...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: [
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
Checkbox(
|
||||
value: _selectedItems.contains(equipment.equipment.id ?? 0),
|
||||
onChanged: (bool? value) {
|
||||
if (equipment.equipment.id != null) {
|
||||
_onItemSelected(equipment.equipment.id!, value ?? false);
|
||||
}
|
||||
},
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 40,
|
||||
),
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'${((_controller.currentPage - 1) * _controller.pageSize) + index + 1}',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 제조사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.manufacturer,
|
||||
equipment.equipment.manufacturer,
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 장비번호
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.equipmentNumber, // name → equipmentNumber (메인 필드)
|
||||
equipment.equipment.equipmentNumber,
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 모델명
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.modelName ?? '-', // 모델명 표시
|
||||
equipment.equipment.modelName ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
// 상세 정보 (조건부)
|
||||
if (_showDetailedColumns) ...[
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.serialNumber ?? '-',
|
||||
equipment.equipment.serialNumber ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
),
|
||||
],
|
||||
// 수량
|
||||
_buildDataCell(
|
||||
Text(
|
||||
equipment.equipment.quantity.toString(),
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 상태
|
||||
_buildDataCell(
|
||||
_buildStatusBadge(equipment.status),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 입출고일
|
||||
_buildDataCell(
|
||||
_buildCreatedDateWidget(equipment),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 관리
|
||||
_buildDataCell(
|
||||
_buildActionButtons(equipment.equipment.id ?? 0),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 90,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1051,6 +1063,60 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 재고 상태 위젯 빌더 (백엔드 기반 단순화)
|
||||
Widget _buildInventoryStatus(UnifiedEquipment equipment) {
|
||||
// 백엔드 Equipment_History 기반으로 단순 상태만 표시
|
||||
Widget stockInfo;
|
||||
if (equipment.status == EquipmentStatus.in_) {
|
||||
// 입고 상태: 재고 있음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'보유중',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.green[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == EquipmentStatus.out) {
|
||||
// 출고 상태: 재고 없음
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.orange, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'출고됨',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.orange[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (equipment.status == EquipmentStatus.rent) {
|
||||
// 대여 상태
|
||||
stockInfo = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.schedule, color: Colors.blue, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'대여중',
|
||||
style: ShadcnTheme.bodySmall.copyWith(color: Colors.blue[700]),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// 기타 상태
|
||||
stockInfo = Text(
|
||||
'-',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
);
|
||||
}
|
||||
|
||||
return stockInfo;
|
||||
}
|
||||
|
||||
/// 상태 배지 빌더
|
||||
Widget _buildStatusBadge(String status) {
|
||||
String displayText;
|
||||
@@ -1148,7 +1214,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final result = await EquipmentHistoryDialog.show(
|
||||
context: context,
|
||||
equipmentId: equipmentId,
|
||||
equipmentName: '${equipment.equipment.manufacturer} ${equipment.equipment.equipmentNumber}', // name → equipmentNumber
|
||||
equipmentName: '${equipment.equipment.model?.vendor?.name ?? 'N/A'} ${equipment.equipment.serialNumber}', // Vendor + Equipment Number
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
@@ -1156,15 +1222,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
}
|
||||
|
||||
// 편집 다이얼로그 표시
|
||||
void _showEditDialog(UnifiedEquipment equipment) {
|
||||
_handleEdit(equipment);
|
||||
}
|
||||
|
||||
// 삭제 다이얼로그 표시
|
||||
void _showDeleteDialog(UnifiedEquipment equipment) {
|
||||
_handleDelete(equipment);
|
||||
}
|
||||
|
||||
// 편집 핸들러 (액션 버튼에서 호출) - 장비 ID로 처리
|
||||
void _handleEditById(int equipmentId) {
|
||||
@@ -1197,27 +1254,45 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 페이지 데이터 가져오기
|
||||
List<UnifiedEquipment> _getPagedEquipments() {
|
||||
// 서버 페이지네이션 사용: 컨트롤러의 items가 이미 페이지네이션된 데이터
|
||||
// 로컬 필터링만 적용
|
||||
return _getFilteredEquipments();
|
||||
}
|
||||
|
||||
// 사용하지 않는 카테고리 관련 함수들 제거됨 (리스트 API에서 제공하지 않음)
|
||||
|
||||
/// 캐시된 데이터를 사용한 상태 드롭다운 아이템 생성
|
||||
List<DropdownMenuItem<String>> _buildStatusDropdownItems() {
|
||||
List<DropdownMenuItem<String>> items = [
|
||||
const DropdownMenuItem(value: 'all', child: Text('전체')),
|
||||
/// 상태 표시 텍스트 가져오기
|
||||
String _getStatusDisplayText(String status) {
|
||||
switch (status) {
|
||||
case 'all':
|
||||
return '전체';
|
||||
case 'in':
|
||||
return '입고';
|
||||
case 'out':
|
||||
return '출고';
|
||||
case 'rent':
|
||||
return '대여';
|
||||
case 'repair':
|
||||
return '수리중';
|
||||
case 'damaged':
|
||||
return '손상';
|
||||
case 'lost':
|
||||
return '분실';
|
||||
case 'disposed':
|
||||
return '폐기';
|
||||
default:
|
||||
return '전체';
|
||||
}
|
||||
}
|
||||
|
||||
/// 캐시된 데이터를 사용한 상태 선택 옵션 생성
|
||||
List<ShadOption<String>> _buildStatusSelectOptions() {
|
||||
List<ShadOption<String>> options = [
|
||||
const ShadOption(value: 'all', child: Text('전체')),
|
||||
];
|
||||
|
||||
// 캐시된 상태 데이터에서 드롭다운 아이템 생성
|
||||
// 캐시된 상태 데이터에서 선택 옵션 생성
|
||||
final cachedStatuses = _controller.getCachedEquipmentStatuses();
|
||||
|
||||
for (final status in cachedStatuses) {
|
||||
items.add(
|
||||
DropdownMenuItem(
|
||||
options.add(
|
||||
ShadOption(
|
||||
value: status.id,
|
||||
child: Text(status.name),
|
||||
),
|
||||
@@ -1226,18 +1301,18 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
// 캐시된 데이터가 없을 때 폴백으로 하드코딩된 상태 사용
|
||||
if (cachedStatuses.isEmpty) {
|
||||
items.addAll([
|
||||
const DropdownMenuItem(value: 'in', child: Text('입고')),
|
||||
const DropdownMenuItem(value: 'out', child: Text('출고')),
|
||||
const DropdownMenuItem(value: 'rent', child: Text('대여')),
|
||||
const DropdownMenuItem(value: 'repair', child: Text('수리중')),
|
||||
const DropdownMenuItem(value: 'damaged', child: Text('손상')),
|
||||
const DropdownMenuItem(value: 'lost', child: Text('분실')),
|
||||
const DropdownMenuItem(value: 'disposed', child: Text('폐기')),
|
||||
options.addAll([
|
||||
const ShadOption(value: 'in', child: Text('입고')),
|
||||
const ShadOption(value: 'out', child: Text('출고')),
|
||||
const ShadOption(value: 'rent', child: Text('대여')),
|
||||
const ShadOption(value: 'repair', child: Text('수리중')),
|
||||
const ShadOption(value: 'damaged', child: Text('손상')),
|
||||
const ShadOption(value: 'lost', child: Text('분실')),
|
||||
const ShadOption(value: 'disposed', child: Text('폐기')),
|
||||
]);
|
||||
}
|
||||
|
||||
return items;
|
||||
return options;
|
||||
}
|
||||
|
||||
// 사용하지 않는 현재위치, 점검일 관련 함수들 제거됨 (리스트 API에서 제공하지 않음)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/company_branch_info.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_shadcn.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
|
||||
class EquipmentOutFormScreen extends StatefulWidget {
|
||||
@@ -19,12 +17,12 @@ class EquipmentOutFormScreen extends StatefulWidget {
|
||||
final List<Map<String, dynamic>>? selectedEquipments;
|
||||
|
||||
const EquipmentOutFormScreen({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.equipmentOutId,
|
||||
this.selectedEquipment,
|
||||
this.selectedEquipmentInId,
|
||||
this.selectedEquipments,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentOutFormScreen> createState() => _EquipmentOutFormScreenState();
|
||||
@@ -68,11 +66,9 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
}
|
||||
|
||||
// 각 장비별로 전체 폭을 사용하는 리스트로 구현
|
||||
return Container(
|
||||
return SizedBox(
|
||||
width: double.infinity, // 전체 폭 사용
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.zero, // margin 제거
|
||||
child: ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
@@ -157,7 +153,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
);
|
||||
if (picked != null) {
|
||||
equipment.warrantyStartDate = picked;
|
||||
controller.notifyListeners();
|
||||
// controller.notifyListeners() 호출 불필요 - 데이터 변경 시 자동 알림
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -192,7 +188,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
);
|
||||
if (picked != null) {
|
||||
equipment.warrantyEndDate = picked;
|
||||
controller.notifyListeners();
|
||||
// controller.notifyListeners() 호출 불필요 - 데이터 변경 시 자동 알림
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
@@ -254,7 +250,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
title: const Text('장비 출고'),
|
||||
),
|
||||
body: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: ShadProgress(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -286,7 +282,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
ShadButton(
|
||||
onPressed: () {
|
||||
controller.clearError();
|
||||
controller.loadDropdownData();
|
||||
@@ -305,7 +301,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
controller.isEditMode
|
||||
? '장비 출고 수정'
|
||||
: totalSelectedEquipments > 0
|
||||
? '장비 출고 등록 (${totalSelectedEquipments}개)'
|
||||
? '장비 출고 등록 ($totalSelectedEquipments개)'
|
||||
: '장비 출고 등록',
|
||||
),
|
||||
),
|
||||
@@ -323,7 +319,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
_buildSummaryTable(controller)
|
||||
else if (controller.selectedEquipment != null)
|
||||
// 단일 장비 요약 카드도 전체 폭으로 맞춤
|
||||
Container(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: EquipmentSingleSummaryCard(
|
||||
equipment: controller.selectedEquipment!,
|
||||
@@ -334,7 +330,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 요약 카드 아래 라디오 버튼 추가
|
||||
const SizedBox(height: 12),
|
||||
// 전체 폭을 사용하는 라디오 버튼
|
||||
Container(width: double.infinity, child: _buildOutTypeRadio(controller)),
|
||||
SizedBox(width: double.infinity, child: _buildOutTypeRadio(controller)),
|
||||
const SizedBox(height: 16),
|
||||
// 출고 정보 입력 섹션 (수정/등록)
|
||||
_buildOutgoingInfoSection(context, controller),
|
||||
@@ -380,7 +376,7 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 저장 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
child: ShadButton(
|
||||
onPressed:
|
||||
canSubmit
|
||||
? () {
|
||||
@@ -408,10 +404,10 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
|
||||
controller.saveEquipmentOut(context).then((success) {
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('출고가 완료되었습니다.'),
|
||||
duration: Duration(seconds: 2),
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('출고 완료'),
|
||||
description: const Text('출고가 완료되었습니다.'),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
@@ -419,22 +415,8 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
});
|
||||
}
|
||||
: null,
|
||||
style:
|
||||
canSubmit
|
||||
? ElevatedButton.styleFrom(
|
||||
backgroundColor: ShadcnTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
)
|
||||
: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
controller.isEditMode ? '수정하기' : '등록하기',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
child: Text(
|
||||
controller.isEditMode ? '수정하기' : '등록하기',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -470,25 +452,19 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
// 장비 상태 변경 (출고 시 'inuse'로 자동 설정)
|
||||
const Text('장비 상태 설정', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
DropdownButtonFormField<String>(
|
||||
value: 'inuse', // 출고 시 기본값
|
||||
decoration: const InputDecoration(
|
||||
hintText: '출고 후 장비 상태',
|
||||
labelText: '출고 후 상태 *',
|
||||
ShadSelect<String>(
|
||||
initialValue: 'inuse', // 출고 시 기본값
|
||||
placeholder: const Text('출고 후 장비 상태'),
|
||||
selectedOptionBuilder: (context, value) => Text(
|
||||
value == 'inuse' ? '사용 중' : '유지보수',
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'inuse', child: Text('사용 중')),
|
||||
DropdownMenuItem(value: 'maintenance', child: Text('유지보수')),
|
||||
options: const [
|
||||
ShadOption(value: 'inuse', child: Text('사용 중')),
|
||||
ShadOption(value: 'maintenance', child: Text('유지보수')),
|
||||
],
|
||||
onChanged: (value) {
|
||||
// controller.equipmentStatus = value; // TODO: 컨트롤러에 추가 필요
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '출고 후 상태를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -517,30 +493,23 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
...List.generate(controller.selectedCompanies.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: controller.selectedCompanies[index],
|
||||
decoration: InputDecoration(
|
||||
hintText: index == 0 ? '출고할 회사를 선택하세요' : '추가된 출고할 회사를 선택하세요',
|
||||
// 이전 드롭다운에 값이 선택되지 않았으면 비활성화
|
||||
enabled:
|
||||
index == 0 ||
|
||||
controller.selectedCompanies[index - 1] != null,
|
||||
),
|
||||
items:
|
||||
child: ShadSelect<String>(
|
||||
initialValue: controller.selectedCompanies[index],
|
||||
placeholder: Text(index == 0 ? '출고할 회사를 선택하세요' : '추가된 출고할 회사를 선택하세요'),
|
||||
enabled:
|
||||
index == 0 ||
|
||||
controller.selectedCompanies[index - 1] != null,
|
||||
selectedOptionBuilder: (context, value) =>
|
||||
_buildCompanyDropdownItem(value ?? '', controller),
|
||||
options:
|
||||
controller.availableCompaniesPerDropdown[index]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
(item) => ShadOption(
|
||||
value: item.name,
|
||||
child: _buildCompanyDropdownItem(item.name, controller),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
validator: (value) {
|
||||
if (index == 0 && (value == null || value.isEmpty)) {
|
||||
return '출고 회사를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged:
|
||||
(index == 0 ||
|
||||
controller.selectedCompanies[index - 1] != null)
|
||||
@@ -740,19 +709,19 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(hintText: hint),
|
||||
items:
|
||||
ShadSelect<String>(
|
||||
initialValue: value,
|
||||
placeholder: Text(hint),
|
||||
selectedOptionBuilder: (context, value) => Text(value ?? ''),
|
||||
options:
|
||||
items
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
(item) => ShadOption(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -835,23 +804,6 @@ class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// 회사 ID에 따른 담당자 정보를 가져와 표시하는 위젯 목록 생성
|
||||
List<Widget> _getUsersForCompany(CompanyBranchInfo companyInfo) {
|
||||
final List<Widget> userWidgets = [];
|
||||
|
||||
// 판교지점 특별 처리
|
||||
if (companyInfo.originalName == "판교지점" &&
|
||||
companyInfo.parentCompanyName == "LG전자") {
|
||||
userWidgets.add(
|
||||
Text(
|
||||
'정수진 사원 010-4567-8901 jung.soojin@lg.com',
|
||||
style: ShadcnTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return userWidgets;
|
||||
}
|
||||
|
||||
// 출고/대여/폐기 라디오 버튼 위젯
|
||||
Widget _buildOutTypeRadio(EquipmentOutFormController controller) {
|
||||
|
||||
@@ -14,7 +14,7 @@ class AutocompleteTextField extends StatefulWidget {
|
||||
final FocusNode? focusNode;
|
||||
|
||||
const AutocompleteTextField({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.items,
|
||||
@@ -23,7 +23,7 @@ class AutocompleteTextField extends StatefulWidget {
|
||||
this.isRequired = false,
|
||||
this.hintText = '',
|
||||
this.focusNode,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<AutocompleteTextField> createState() => _AutocompleteTextFieldState();
|
||||
@@ -141,7 +141,7 @@ class _AutocompleteTextFieldState extends State<AutocompleteTextField> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
color: Colors.grey.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ class CustomDropdownField extends StatefulWidget {
|
||||
final GlobalKey fieldKey;
|
||||
|
||||
const CustomDropdownField({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.hint,
|
||||
required this.required,
|
||||
@@ -29,7 +29,7 @@ class CustomDropdownField extends StatefulWidget {
|
||||
required this.onDropdownPressed,
|
||||
required this.layerLink,
|
||||
required this.fieldKey,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomDropdownField> createState() => _CustomDropdownFieldState();
|
||||
|
||||
@@ -193,13 +193,14 @@ class EquipmentBasicInfoSection extends StatelessWidget {
|
||||
focusNode: nameFieldFocusNode,
|
||||
items: controller.equipmentNames,
|
||||
onChanged: (value) {
|
||||
controller.name = value;
|
||||
// Equipment name은 model 선택으로 자동 설정됨
|
||||
// controller.name = value;
|
||||
},
|
||||
onFieldSubmitted: (value) {
|
||||
final suggestion = getEquipmentNameAutocompleteSuggestion(value);
|
||||
if (suggestion != null && suggestion.length > value.length) {
|
||||
equipmentNameController.text = suggestion;
|
||||
controller.name = suggestion;
|
||||
// controller.name = suggestion;
|
||||
equipmentNameController.selection = TextSelection.collapsed(
|
||||
offset: suggestion.length,
|
||||
);
|
||||
|
||||
@@ -12,10 +12,10 @@ class EquipmentHistoryDialog extends StatefulWidget {
|
||||
final String equipmentName;
|
||||
|
||||
const EquipmentHistoryDialog({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.equipmentId,
|
||||
required this.equipmentName,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentHistoryDialog> createState() => _EquipmentHistoryDialogState();
|
||||
@@ -138,9 +138,9 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
||||
|
||||
setState(() {
|
||||
if (isRefresh) {
|
||||
_histories = histories;
|
||||
_histories = histories.cast<EquipmentHistoryDto>();
|
||||
} else {
|
||||
_histories.addAll(histories);
|
||||
_histories.addAll(histories.cast<EquipmentHistoryDto>());
|
||||
}
|
||||
_filterHistories();
|
||||
_hasMore = histories.length == _perPage;
|
||||
@@ -217,7 +217,7 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.02),
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -239,7 +239,7 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: typeColor.withOpacity(0.1),
|
||||
color: typeColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
@@ -379,7 +379,7 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
|
||||
402
lib/screens/equipment/widgets/equipment_history_panel.dart
Normal file
402
lib/screens/equipment/widgets/equipment_history_panel.dart
Normal file
@@ -0,0 +1,402 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_history_controller.dart';
|
||||
import 'package:superport/data/models/equipment_history_dto.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Equipment의 입출고 이력을 표시하는 패널 위젯
|
||||
class EquipmentHistoryPanel extends StatefulWidget {
|
||||
final Equipment equipment;
|
||||
final bool isExpanded;
|
||||
final VoidCallback? onToggleExpand;
|
||||
|
||||
const EquipmentHistoryPanel({
|
||||
super.key,
|
||||
required this.equipment,
|
||||
this.isExpanded = false,
|
||||
this.onToggleExpand,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentHistoryPanel> createState() => _EquipmentHistoryPanelState();
|
||||
}
|
||||
|
||||
class _EquipmentHistoryPanelState extends State<EquipmentHistoryPanel> {
|
||||
late EquipmentHistoryController _controller;
|
||||
bool _isLoading = false;
|
||||
List<EquipmentHistoryDto> _histories = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = GetIt.instance<EquipmentHistoryController>();
|
||||
if (widget.equipment.id != null) {
|
||||
_loadHistory();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadHistory() async {
|
||||
if (widget.equipment.id == null) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Equipment ID로 이력 조회
|
||||
await _controller.searchEquipmentHistories(
|
||||
equipmentId: widget.equipment.id,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_histories = _controller.historyList;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더
|
||||
InkWell(
|
||||
onTap: widget.onToggleExpand,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history,
|
||||
size: 20,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'장비 이력',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_histories.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${_histories.length}건',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
widget.isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 확장된 내용
|
||||
if (widget.isExpanded) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 현재 상태 요약
|
||||
_buildCurrentStatusSummary(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 이력 표시
|
||||
if (_isLoading)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (_histories.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.history,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'입출고 이력이 없습니다',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildHistoryList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentStatusSummary() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.blue[200]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 20,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'현재 장비 정보',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blue[900],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'장비번호: ${widget.equipment.serialNumber}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
if (widget.equipment.model != null) ...[
|
||||
Text(
|
||||
'제조사: ${widget.equipment.model?.vendor?.name ?? 'N/A'}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'모델: ${widget.equipment.model?.name ?? 'N/A'}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.equipment.serialNumber != null)
|
||||
Text(
|
||||
'시리얼: ${widget.equipment.serialNumber}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.blue[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryList() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'입출고 이력',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 테이블 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(7),
|
||||
topRight: Radius.circular(7),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('거래유형',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700])),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('날짜',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700])),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text('수량',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700])),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('창고',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700])),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text('비고',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
color: Colors.grey[700])),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 테이블 바디
|
||||
..._histories.map((history) => _buildHistoryRow(history)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryRow(EquipmentHistoryDto history) {
|
||||
final dateFormat = DateFormat('yyyy-MM-dd');
|
||||
final isIn = history.transactionType == 'I';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey[200]!),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isIn ? Icons.arrow_downward : Icons.arrow_upward,
|
||||
size: 16,
|
||||
color: isIn ? Colors.green : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isIn ? '입고' : '출고',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isIn ? Colors.green[700] : Colors.red[700],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
dateFormat.format(history.transactedAt),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
history.quantity?.toString() ?? '-',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'-', // 창고 정보는 백엔드에서 제공하지 않음
|
||||
style: const TextStyle(fontSize: 13),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
history.remark ?? '-',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class EquipmentMultiSummaryCard extends StatelessWidget {
|
||||
...selectedEquipments.map((equipmentData) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
return EquipmentSingleSummaryCard(equipment: equipment);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class EquipmentSingleSummaryCard extends StatelessWidget {
|
||||
border: Border.all(color: Colors.blue.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'수량: ${equipment.quantity}',
|
||||
'수량: 1',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
@@ -110,10 +110,10 @@ class EquipmentSingleSummaryCard extends StatelessWidget {
|
||||
: '정보 없음',
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '카테고리',
|
||||
label: '모델명',
|
||||
value:
|
||||
equipment.category.isNotEmpty
|
||||
? '${equipment.category} > ${equipment.subCategory} > ${equipment.subSubCategory}'
|
||||
equipment.modelName.isNotEmpty
|
||||
? equipment.modelName
|
||||
: '정보 없음',
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
@@ -126,19 +126,13 @@ class EquipmentSingleSummaryCard extends StatelessWidget {
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '출고 수량',
|
||||
value: equipment.quantity.toString(),
|
||||
value: '1',
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '입고일',
|
||||
value: _formatDate(equipment.inDate),
|
||||
),
|
||||
// 워런티 정보 추가
|
||||
if (equipment.warrantyLicense != null &&
|
||||
equipment.warrantyLicense!.isNotEmpty)
|
||||
EquipmentSummaryRow(
|
||||
label: '워런티 라이센스',
|
||||
value: equipment.warrantyLicense!,
|
||||
),
|
||||
// 워런티 라이센스 필드는 백엔드에서 제거됨
|
||||
EquipmentSummaryRow(
|
||||
label: '워런티 시작일',
|
||||
value: _formatDate(equipment.warrantyStartDate),
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/screens/vendor/controllers/vendor_controller.dart';
|
||||
import 'package:superport/screens/model/controllers/model_controller.dart';
|
||||
import 'package:superport/injection_container.dart';
|
||||
|
||||
/// Equipment 등록/수정 폼에서 사용할 Vendor→Model cascade 선택 위젯
|
||||
class EquipmentVendorModelSelector extends StatefulWidget {
|
||||
final int? initialVendorId;
|
||||
final int? initialModelId;
|
||||
final Function(int? vendorId, int? modelId) onChanged;
|
||||
final bool isReadOnly;
|
||||
|
||||
const EquipmentVendorModelSelector({
|
||||
super.key,
|
||||
this.initialVendorId,
|
||||
this.initialModelId,
|
||||
required this.onChanged,
|
||||
this.isReadOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentVendorModelSelector> createState() => _EquipmentVendorModelSelectorState();
|
||||
}
|
||||
|
||||
class _EquipmentVendorModelSelectorState extends State<EquipmentVendorModelSelector> {
|
||||
late VendorController _vendorController;
|
||||
late ModelController _modelController;
|
||||
|
||||
int? _selectedVendorId;
|
||||
int? _selectedModelId;
|
||||
List<ModelDto> _filteredModels = [];
|
||||
bool _isLoadingVendors = false;
|
||||
bool _isLoadingModels = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_vendorController = getIt<VendorController>();
|
||||
_modelController = getIt<ModelController>();
|
||||
|
||||
_selectedVendorId = widget.initialVendorId;
|
||||
_selectedModelId = widget.initialModelId;
|
||||
|
||||
_loadInitialData();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialData() async {
|
||||
setState(() => _isLoadingVendors = true);
|
||||
|
||||
try {
|
||||
// Vendor 목록 로드
|
||||
await _vendorController.loadVendors();
|
||||
|
||||
// 초기 vendor가 설정되어 있으면 해당 vendor의 모델 로드
|
||||
if (_selectedVendorId != null) {
|
||||
await _loadModelsForVendor(_selectedVendorId!);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('데이터 로드 실패: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingVendors = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadModelsForVendor(int vendorId) async {
|
||||
setState(() {
|
||||
_isLoadingModels = true;
|
||||
_filteredModels = [];
|
||||
_selectedModelId = null; // Vendor 변경 시 Model 선택 초기화
|
||||
});
|
||||
|
||||
try {
|
||||
// 특정 vendor의 모델 목록 로드
|
||||
await _modelController.refreshModels();
|
||||
|
||||
// vendor에 해당하는 모델만 필터링
|
||||
final allModels = _modelController.allModels;
|
||||
_filteredModels = allModels.where((model) => model.vendorsId == vendorId).toList();
|
||||
|
||||
// 초기 모델이 설정되어 있고 필터링된 목록에 있으면 유지
|
||||
if (widget.initialModelId != null &&
|
||||
_filteredModels.any((m) => m.id == widget.initialModelId)) {
|
||||
_selectedModelId = widget.initialModelId;
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('모델 로드 실패: ${e.toString()}'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isLoadingModels = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onVendorChanged(int? vendorId) {
|
||||
setState(() {
|
||||
_selectedVendorId = vendorId;
|
||||
_selectedModelId = null; // Vendor 변경 시 Model 초기화
|
||||
});
|
||||
|
||||
if (vendorId != null) {
|
||||
_loadModelsForVendor(vendorId);
|
||||
} else {
|
||||
setState(() {
|
||||
_filteredModels = [];
|
||||
});
|
||||
}
|
||||
|
||||
// 변경사항 콜백
|
||||
widget.onChanged(_selectedVendorId, _selectedModelId);
|
||||
}
|
||||
|
||||
void _onModelChanged(int? modelId) {
|
||||
setState(() {
|
||||
_selectedModelId = modelId;
|
||||
});
|
||||
|
||||
// 변경사항 콜백
|
||||
widget.onChanged(_selectedVendorId, _selectedModelId);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Vendor 선택 드롭다운
|
||||
_buildVendorDropdown(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Model 선택 드롭다운 (Vendor 선택 후 활성화)
|
||||
_buildModelDropdown(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVendorDropdown() {
|
||||
if (_isLoadingVendors) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final vendors = _vendorController.vendors;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.isReadOnly ? '제조사 * 🔒' : '제조사 *',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadSelect<int>(
|
||||
placeholder: const Text('제조사를 선택하세요'),
|
||||
options: vendors.map((vendor) {
|
||||
return ShadOption(
|
||||
value: vendor.id!,
|
||||
child: Text(vendor.name),
|
||||
);
|
||||
}).toList(),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
final vendor = vendors.firstWhere((v) => v.id == value);
|
||||
return Text(vendor.name);
|
||||
},
|
||||
onChanged: widget.isReadOnly ? null : _onVendorChanged,
|
||||
initialValue: _selectedVendorId,
|
||||
enabled: !widget.isReadOnly,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModelDropdown() {
|
||||
if (_isLoadingModels) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Vendor가 선택되지 않으면 비활성화
|
||||
final isEnabled = !widget.isReadOnly && _selectedVendorId != null;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.isReadOnly ? '모델 * 🔒' : '모델 *',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ShadSelect<int>(
|
||||
placeholder: Text(
|
||||
_selectedVendorId == null
|
||||
? '먼저 제조사를 선택하세요'
|
||||
: '모델을 선택하세요'
|
||||
),
|
||||
options: _filteredModels.map((model) {
|
||||
return ShadOption(
|
||||
value: model.id,
|
||||
child: Text(model.name),
|
||||
);
|
||||
}).toList(),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
final model = _filteredModels.firstWhere((m) => m.id == value);
|
||||
return Text(model.name);
|
||||
},
|
||||
onChanged: isEnabled ? _onModelChanged : null,
|
||||
initialValue: _selectedModelId,
|
||||
enabled: isEnabled,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user