backup: 사용하지 않는 파일 삭제 전 복구 지점
- 전체 371개 파일 중 82개 미사용 파일 식별 - Phase 1: 33개 파일 삭제 예정 (100% 안전) - Phase 2: 30개 파일 삭제 검토 예정 - Phase 3: 19개 파일 수동 검토 예정 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
280
lib/screens/equipment/controllers/equipment_controller.dart
Normal file
280
lib/screens/equipment/controllers/equipment_controller.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
|
||||
import '../../../data/models/equipment/equipment_dto.dart';
|
||||
import '../../../domain/usecases/equipment/get_equipments_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/get_equipment_detail_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/create_equipment_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/update_equipment_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/delete_equipment_usecase.dart';
|
||||
import '../../../domain/usecases/equipment/restore_equipment_usecase.dart';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
|
||||
@injectable
|
||||
class EquipmentController with ChangeNotifier {
|
||||
final GetEquipmentsUseCase _getEquipmentsUseCase;
|
||||
final GetEquipmentDetailUseCase _getEquipmentDetailUseCase;
|
||||
final CreateEquipmentUseCase _createEquipmentUseCase;
|
||||
final UpdateEquipmentUseCase _updateEquipmentUseCase;
|
||||
final DeleteEquipmentUseCase _deleteEquipmentUseCase;
|
||||
final RestoreEquipmentUseCase _restoreEquipmentUseCase;
|
||||
|
||||
// 상태 관리
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
List<EquipmentDto> _equipments = [];
|
||||
EquipmentDto? _selectedEquipment;
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _totalPages = 0;
|
||||
int _totalItems = 0;
|
||||
final int _pageSize = AppConstants.equipmentPageSize;
|
||||
|
||||
// 필터
|
||||
String? _searchQuery;
|
||||
bool _includeDeleted = false;
|
||||
|
||||
EquipmentController(
|
||||
this._getEquipmentsUseCase,
|
||||
this._getEquipmentDetailUseCase,
|
||||
this._createEquipmentUseCase,
|
||||
this._updateEquipmentUseCase,
|
||||
this._deleteEquipmentUseCase,
|
||||
this._restoreEquipmentUseCase,
|
||||
);
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
bool get hasError => _error != null;
|
||||
List<EquipmentDto> get equipments => _equipments;
|
||||
EquipmentDto? get selectedEquipment => _selectedEquipment;
|
||||
int get currentPage => _currentPage;
|
||||
int get totalPages => _totalPages;
|
||||
int get totalItems => _totalItems;
|
||||
int get pageSize => _pageSize;
|
||||
String? get searchQuery => _searchQuery;
|
||||
bool get includeDeleted => _includeDeleted;
|
||||
|
||||
void _setLoading(bool loading) {
|
||||
_isLoading = loading;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _setError(String? error) {
|
||||
_error = error;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 장비 목록 조회
|
||||
Future<void> loadEquipments({
|
||||
int page = 1,
|
||||
int perPage = AppConstants.equipmentPageSize,
|
||||
String? search,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
try {
|
||||
if (refresh) {
|
||||
_equipments.clear();
|
||||
_currentPage = 1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
_setLoading(true);
|
||||
|
||||
final params = GetEquipmentsParams(
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
search: search,
|
||||
);
|
||||
|
||||
final response = await _getEquipmentsUseCase(params);
|
||||
|
||||
response.fold(
|
||||
(failure) => _setError('장비 목록을 불러오는데 실패했습니다: ${failure.message}'),
|
||||
(data) {
|
||||
_equipments = data.items;
|
||||
_currentPage = page;
|
||||
_totalPages = data.totalPages;
|
||||
_totalItems = data.totalElements;
|
||||
_searchQuery = search;
|
||||
clearError();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('장비 목록을 불러오는데 실패했습니다: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 상세 조회
|
||||
Future<void> loadEquipment(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _getEquipmentDetailUseCase(id);
|
||||
|
||||
response.fold(
|
||||
(failure) => _setError('장비 정보를 불러오는데 실패했습니다: ${failure.message}'),
|
||||
(equipment) {
|
||||
_selectedEquipment = equipment;
|
||||
clearError();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('장비 정보를 불러오는데 실패했습니다: $e');
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 생성
|
||||
Future<bool> createEquipment(EquipmentRequestDto request) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _createEquipmentUseCase(request);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('장비 생성에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(equipment) {
|
||||
// 목록 새로고침
|
||||
loadEquipments(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('장비 생성에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 수정
|
||||
Future<bool> updateEquipment(int id, EquipmentUpdateRequestDto request) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final params = UpdateEquipmentParams(id: id, request: request);
|
||||
final response = await _updateEquipmentUseCase(params);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('장비 수정에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(equipment) {
|
||||
// 목록 새로고침
|
||||
loadEquipments(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('장비 수정에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 삭제 (Soft Delete)
|
||||
Future<bool> deleteEquipment(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _deleteEquipmentUseCase(id);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('장비 삭제에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(_) {
|
||||
// 목록 새로고침
|
||||
loadEquipments(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('장비 삭제에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 복구
|
||||
Future<bool> restoreEquipment(int id) async {
|
||||
try {
|
||||
_setLoading(true);
|
||||
|
||||
final response = await _restoreEquipmentUseCase(id);
|
||||
|
||||
return response.fold(
|
||||
(failure) {
|
||||
_setError('장비 복구에 실패했습니다: ${failure.message}');
|
||||
return false;
|
||||
},
|
||||
(equipment) {
|
||||
// 목록 새로고침
|
||||
loadEquipments(refresh: true);
|
||||
clearError();
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_setError('장비 복구에 실패했습니다: $e');
|
||||
return false;
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 페이지 변경
|
||||
Future<void> goToPage(int page) async {
|
||||
if (page < 1 || page > _totalPages || page == _currentPage) return;
|
||||
|
||||
await loadEquipments(
|
||||
page: page,
|
||||
search: _searchQuery,
|
||||
);
|
||||
}
|
||||
|
||||
/// 검색 설정
|
||||
void setSearch(String? search) {
|
||||
_searchQuery = search;
|
||||
loadEquipments(
|
||||
search: search,
|
||||
refresh: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 새로고침
|
||||
Future<void> refresh() async {
|
||||
await loadEquipments(
|
||||
page: 1,
|
||||
search: _searchQuery,
|
||||
refresh: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 선택된 장비 초기화
|
||||
void clearSelectedEquipment() {
|
||||
_selectedEquipment = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ 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 '../../../data/models/model/model_dto.dart';
|
||||
import '../../../data/models/vendor_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 '../../../domain/usecases/models/get_models_usecase.dart';
|
||||
import '../../../domain/usecases/vendor_usecase.dart';
|
||||
import '../../../core/errors/failures.dart';
|
||||
|
||||
/// 장비 폼 컨트롤러 (생성/수정)
|
||||
@@ -18,19 +20,22 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
final UpdateEquipmentUseCase _updateEquipmentUseCase;
|
||||
final GetEquipmentDetailUseCase _getEquipmentDetailUseCase;
|
||||
final GetCompaniesUseCase _getCompaniesUseCase;
|
||||
final ModelUseCase _modelUseCase;
|
||||
final GetModelsUseCase _getModelsUseCase;
|
||||
final VendorUseCase _vendorUseCase;
|
||||
|
||||
EquipmentFormController(
|
||||
this._createEquipmentUseCase,
|
||||
this._updateEquipmentUseCase,
|
||||
this._getEquipmentDetailUseCase,
|
||||
this._getCompaniesUseCase,
|
||||
this._modelUseCase,
|
||||
this._getModelsUseCase,
|
||||
this._vendorUseCase,
|
||||
);
|
||||
|
||||
// 상태 관리
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingCompanies = false;
|
||||
bool _isLoadingVendors = false;
|
||||
bool _isLoadingModels = false;
|
||||
bool _isSaving = false;
|
||||
String? _error;
|
||||
@@ -41,11 +46,13 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
|
||||
// 드롭다운 데이터
|
||||
List<CompanyDto> _companies = [];
|
||||
List<VendorDto> _vendors = [];
|
||||
List<ModelDto> _models = [];
|
||||
List<ModelDto> _filteredModels = [];
|
||||
|
||||
// 선택된 값
|
||||
int? _selectedCompanyId;
|
||||
int? _selectedVendorId;
|
||||
int? _selectedModelId;
|
||||
|
||||
// 폼 컨트롤러들
|
||||
@@ -57,12 +64,13 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
|
||||
// 날짜 필드들
|
||||
DateTime? _purchasedAt;
|
||||
DateTime _warrantyStartedAt = DateTime.now();
|
||||
DateTime _warrantyEndedAt = DateTime.now().add(const Duration(days: 365));
|
||||
DateTime _warrantyStartedAt = DateTime.now().toUtc();
|
||||
DateTime _warrantyEndedAt = DateTime.now().toUtc().add(const Duration(days: 365));
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isLoadingCompanies => _isLoadingCompanies;
|
||||
bool get isLoadingVendors => _isLoadingVendors;
|
||||
bool get isLoadingModels => _isLoadingModels;
|
||||
bool get isSaving => _isSaving;
|
||||
String? get error => _error;
|
||||
@@ -70,9 +78,11 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
bool get isEditMode => _equipmentId != null;
|
||||
|
||||
List<CompanyDto> get companies => _companies;
|
||||
List<VendorDto> get vendors => _vendors;
|
||||
List<ModelDto> get filteredModels => _filteredModels;
|
||||
|
||||
int? get selectedCompanyId => _selectedCompanyId;
|
||||
int? get selectedVendorId => _selectedVendorId;
|
||||
int? get selectedModelId => _selectedModelId;
|
||||
|
||||
DateTime? get purchasedAt => _purchasedAt;
|
||||
@@ -105,10 +115,11 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 초기 데이터 로드 (회사, 모델)
|
||||
/// 초기 데이터 로드 (회사, 제조사, 모델)
|
||||
Future<void> _loadInitialData() async {
|
||||
await Future.wait([
|
||||
_loadCompanies(),
|
||||
_loadVendors(),
|
||||
_loadModels(),
|
||||
]);
|
||||
}
|
||||
@@ -142,14 +153,41 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 제조사 목록 로드
|
||||
Future<void> _loadVendors() async {
|
||||
_isLoadingVendors = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final vendorResponse = await _vendorUseCase.getVendors(limit: 1000); // 모든 제조사 가져오기
|
||||
_vendors = (vendorResponse.items as List)
|
||||
.whereType<VendorDto>()
|
||||
.where((vendor) => vendor.isActive)
|
||||
.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
} catch (e) {
|
||||
_error = '제조사 목록을 불러오는데 실패했습니다: $e';
|
||||
} finally {
|
||||
_isLoadingVendors = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 모델 목록 로드
|
||||
Future<void> _loadModels() async {
|
||||
_isLoadingModels = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
_models = await _modelUseCase.getModels();
|
||||
_filteredModels = _models;
|
||||
const params = GetModelsParams(page: 1, perPage: 1000);
|
||||
final result = await _getModelsUseCase(params);
|
||||
result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(modelResponse) {
|
||||
_models = modelResponse.items;
|
||||
_filteredModels = _models;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_error = '모델 목록을 불러오는데 실패했습니다: $e';
|
||||
} finally {
|
||||
@@ -184,9 +222,9 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
warrantyNumberController.text = equipment.warrantyNumber;
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
|
||||
_purchasedAt = equipment.purchasedAt;
|
||||
_warrantyStartedAt = equipment.warrantyStartedAt;
|
||||
_warrantyEndedAt = equipment.warrantyEndedAt;
|
||||
_purchasedAt = equipment.purchasedAt?.toUtc(); // ✅ UTC 타임존으로 변환
|
||||
_warrantyStartedAt = equipment.warrantyStartedAt.toUtc(); // ✅ UTC 타임존으로 변환
|
||||
_warrantyEndedAt = equipment.warrantyEndedAt.toUtc(); // ✅ UTC 타임존으로 변환
|
||||
|
||||
// 선택된 회사에 따라 모델 필터링
|
||||
_filterModelsByCompany(_selectedCompanyId);
|
||||
@@ -197,8 +235,14 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
/// 회사 선택
|
||||
void selectCompany(int? companyId) {
|
||||
_selectedCompanyId = companyId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 제조사 선택 (제조사별 모델 필터링 활성화)
|
||||
void selectVendor(int? vendorId) {
|
||||
_selectedVendorId = vendorId;
|
||||
_selectedModelId = null; // 모델 선택 초기화
|
||||
_filterModelsByCompany(companyId);
|
||||
_filterModelsByVendor(vendorId); // 실제 제조사별 필터링 실행
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -208,13 +252,23 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 회사별 모델 필터링
|
||||
/// 제조사별 모델 필터링 (실제 구현)
|
||||
void _filterModelsByVendor(int? vendorId) {
|
||||
if (vendorId == null) {
|
||||
_filteredModels = _models;
|
||||
} else {
|
||||
// vendorsId 기준으로 실제 필터링 구현
|
||||
_filteredModels = _models.where((model) => model.vendorsId == vendorId).toList();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 레거시 호환성을 위한 Company 기반 필터링 (현재는 전체 모델 표시)
|
||||
void _filterModelsByCompany(int? companyId) {
|
||||
if (companyId == null) {
|
||||
_filteredModels = _models;
|
||||
} else {
|
||||
// 실제로는 vendor로 필터링해야 하지만,
|
||||
// 현재 구조에서는 모든 모델을 보여주고 사용자가 선택하도록 함
|
||||
// 회사별 모델 필터링은 현재 구조에서는 불가능 (모든 모델 표시)
|
||||
_filteredModels = _models;
|
||||
}
|
||||
notifyListeners();
|
||||
@@ -222,13 +276,13 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
|
||||
/// 구매일 선택
|
||||
void setPurchasedAt(DateTime? date) {
|
||||
_purchasedAt = date;
|
||||
_purchasedAt = date?.toUtc(); // ✅ UTC 타임존으로 변환
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 워런티 시작일 선택
|
||||
void setWarrantyStartedAt(DateTime date) {
|
||||
_warrantyStartedAt = date;
|
||||
_warrantyStartedAt = date.toUtc(); // ✅ UTC 타임존으로 변환
|
||||
// 시작일이 종료일보다 늦으면 종료일을 1년 후로 설정
|
||||
if (_warrantyStartedAt.isAfter(_warrantyEndedAt)) {
|
||||
_warrantyEndedAt = _warrantyStartedAt.add(const Duration(days: 365));
|
||||
@@ -238,7 +292,7 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
|
||||
/// 워런티 종료일 선택
|
||||
void setWarrantyEndedAt(DateTime date) {
|
||||
_warrantyEndedAt = date;
|
||||
_warrantyEndedAt = date.toUtc(); // ✅ UTC 타임존으로 변환
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -298,17 +352,17 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
/// 장비 생성
|
||||
Future<bool> _createEquipment() async {
|
||||
final request = EquipmentRequestDto(
|
||||
companiesId: _selectedCompanyId!,
|
||||
modelsId: _selectedModelId!,
|
||||
companiesId: _selectedCompanyId, // 백엔드: Option<i32> - null 허용
|
||||
modelsId: _selectedModelId, // 백엔드: Option<i32> - null 허용
|
||||
serialNumber: serialNumberController.text.trim(),
|
||||
barcode: barcodeController.text.trim().isNotEmpty
|
||||
? barcodeController.text.trim()
|
||||
: null,
|
||||
purchasedAt: _purchasedAt,
|
||||
purchasedAt: (_purchasedAt ?? DateTime.now()).toUtc(), // 백엔드: 필수 필드 - 기본값 제공
|
||||
purchasePrice: int.tryParse(purchasePriceController.text) ?? 0,
|
||||
warrantyNumber: warrantyNumberController.text.trim(),
|
||||
warrantyStartedAt: _warrantyStartedAt,
|
||||
warrantyEndedAt: _warrantyEndedAt,
|
||||
warrantyStartedAt: _warrantyStartedAt.toUtc(), // ✅ UTC 타임존으로 변환
|
||||
warrantyEndedAt: _warrantyEndedAt.toUtc(), // ✅ UTC 타임존으로 변환
|
||||
remark: remarkController.text.trim().isNotEmpty
|
||||
? remarkController.text.trim()
|
||||
: null,
|
||||
@@ -337,11 +391,11 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
barcode: barcodeController.text.trim().isNotEmpty
|
||||
? barcodeController.text.trim()
|
||||
: null,
|
||||
purchasedAt: _purchasedAt,
|
||||
purchasedAt: _purchasedAt?.toUtc(), // ✅ UTC 타임존으로 변환
|
||||
purchasePrice: int.tryParse(purchasePriceController.text) ?? 0,
|
||||
warrantyNumber: warrantyNumberController.text.trim(),
|
||||
warrantyStartedAt: _warrantyStartedAt,
|
||||
warrantyEndedAt: _warrantyEndedAt,
|
||||
warrantyStartedAt: _warrantyStartedAt.toUtc(), // ✅ UTC 타임존으로 변환
|
||||
warrantyEndedAt: _warrantyEndedAt.toUtc(), // ✅ UTC 타임존으로 변환
|
||||
remark: remarkController.text.trim().isNotEmpty
|
||||
? remarkController.text.trim()
|
||||
: null,
|
||||
@@ -376,8 +430,8 @@ class EquipmentFormController extends ChangeNotifier {
|
||||
remarkController.clear();
|
||||
|
||||
_purchasedAt = null;
|
||||
_warrantyStartedAt = DateTime.now();
|
||||
_warrantyEndedAt = DateTime.now().add(const Duration(days: 365));
|
||||
_warrantyStartedAt = DateTime.now().toUtc(); // ✅ UTC 타임존으로 변환
|
||||
_warrantyEndedAt = DateTime.now().toUtc().add(const Duration(days: 365)); // ✅ UTC 타임존으로 변환
|
||||
|
||||
_error = null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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';
|
||||
import '../../../core/constants/app_constants.dart';
|
||||
|
||||
class EquipmentHistoryController extends ChangeNotifier {
|
||||
final EquipmentHistoryUseCase _useCase;
|
||||
@@ -18,7 +18,7 @@ class EquipmentHistoryController extends ChangeNotifier {
|
||||
|
||||
// 페이지네이션
|
||||
int _currentPage = 1;
|
||||
int _pageSize = PaginationConstants.defaultPageSize;
|
||||
int _pageSize = AppConstants.historyPageSize;
|
||||
int _totalCount = 0;
|
||||
|
||||
// 필터 (백엔드 실제 필드만)
|
||||
@@ -242,7 +242,7 @@ class EquipmentHistoryController extends ChangeNotifier {
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
pageSize: AppConstants.bulkPageSize,
|
||||
transactionType: transactionType,
|
||||
equipmentsId: equipmentId,
|
||||
warehousesId: warehouseId,
|
||||
@@ -286,7 +286,7 @@ class EquipmentHistoryController extends ChangeNotifier {
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
pageSize: AppConstants.maxBulkPageSize,
|
||||
equipmentsId: equipmentId,
|
||||
warehousesId: warehouseId,
|
||||
);
|
||||
@@ -309,7 +309,7 @@ class EquipmentHistoryController extends ChangeNotifier {
|
||||
try {
|
||||
final result = await _useCase.getEquipmentHistories(
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
pageSize: AppConstants.maxBulkPageSize,
|
||||
warehousesId: warehouseId,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async' show unawaited;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:superport/data/models/equipment_history_dto.dart';
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentInFormController extends ChangeNotifier {
|
||||
bool _disposed = false;
|
||||
final EquipmentService _equipmentService = GetIt.instance<EquipmentService>();
|
||||
// final WarehouseService _warehouseService = GetIt.instance<WarehouseService>(); // 사용되지 않음 - 제거
|
||||
// final CompanyService _companyService = GetIt.instance<CompanyService>(); // 사용되지 않음 - 제거
|
||||
@@ -37,15 +39,18 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
/// canSave 상태 업데이트 (UI 렌더링 문제 해결)
|
||||
void _updateCanSave() {
|
||||
if (_disposed) return; // dispose된 경우 업데이트 방지
|
||||
|
||||
final hasEquipmentNumber = _serialNumber.trim().isNotEmpty;
|
||||
final hasModelsId = _modelsId != null; // models_id 필수
|
||||
final hasWarrantyNumber = warrantyNumberController.text.trim().isNotEmpty; // warranty_number 필수
|
||||
final isNotSaving = !_isSaving;
|
||||
|
||||
final newCanSave = isNotSaving && hasEquipmentNumber && hasModelsId;
|
||||
final newCanSave = isNotSaving && hasEquipmentNumber && hasModelsId && hasWarrantyNumber;
|
||||
|
||||
if (_canSave != newCanSave) {
|
||||
_canSave = newCanSave;
|
||||
print('🚀 [canSave 상태 변경] $_canSave → serialNumber: "$_serialNumber", modelsId: $_modelsId');
|
||||
print('🚀 [canSave 상태 변경] $_canSave → serialNumber: "$_serialNumber", modelsId: $_modelsId, warrantyNumber: "${warrantyNumberController.text}"');
|
||||
notifyListeners(); // 명시적 UI 업데이트
|
||||
}
|
||||
}
|
||||
@@ -55,6 +60,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
// 입력 상태 변수 (백엔드 API 구조에 맞게 수정)
|
||||
String _serialNumber = ''; // 장비번호 (필수) - private으로 변경
|
||||
String _barcode = ''; // 바코드 (선택사항) - 새로 추가
|
||||
int? _modelsId; // 모델 ID (필수) - Vendor→Model cascade에서 선택
|
||||
int? _vendorId; // 벤더 ID (UI용, API에는 전송 안함)
|
||||
|
||||
@@ -72,6 +78,14 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
String get barcode => _barcode;
|
||||
set barcode(String value) {
|
||||
if (_barcode != value) {
|
||||
_barcode = value;
|
||||
print('DEBUG [Controller] barcode updated: "$_barcode"');
|
||||
}
|
||||
}
|
||||
|
||||
String get manufacturer => _manufacturer;
|
||||
set manufacturer(String value) {
|
||||
if (_manufacturer != value) {
|
||||
@@ -116,12 +130,13 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
// Vendor→Model 선택 콜백
|
||||
void onVendorModelChanged(int? vendorId, int? modelId) {
|
||||
if (_disposed) return;
|
||||
_vendorId = vendorId;
|
||||
_modelsId = modelId;
|
||||
_updateCanSave();
|
||||
notifyListeners();
|
||||
}
|
||||
DateTime? purchaseDate; // 구매일
|
||||
DateTime? purchaseDate = DateTime.now(); // 구매일 (기본값: 현재 날짜)
|
||||
double? purchasePrice; // 구매가격
|
||||
|
||||
// 삭제된 필드들 (백엔드 미지원)
|
||||
@@ -142,6 +157,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
int _initialStock = 1; // 초기 재고 수량 (기본값: 1)
|
||||
int get initialStock => _initialStock;
|
||||
set initialStock(int value) {
|
||||
if (_disposed) return;
|
||||
if (_initialStock != value && value > 0) {
|
||||
_initialStock = value;
|
||||
notifyListeners();
|
||||
@@ -188,8 +204,17 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
EquipmentInFormController({this.equipmentInId}) {
|
||||
isEditMode = equipmentInId != null;
|
||||
_loadDropdownData();
|
||||
|
||||
// 워런티 번호 기본값 설정
|
||||
if (warrantyNumberController.text.isEmpty) {
|
||||
warrantyNumberController.text = 'WR-${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
_updateCanSave(); // 초기 canSave 상태 설정
|
||||
|
||||
// ✅ 비동기 드롭다운 데이터 로드 시작 (await 불가능하므로 별도 처리)
|
||||
unawaited(_loadDropdownData());
|
||||
|
||||
// 수정 모드일 때 초기 데이터 로드는 initializeForEdit() 메서드로 이동
|
||||
}
|
||||
|
||||
@@ -243,8 +268,28 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
void _processDropdownData(Map<String, dynamic> data) {
|
||||
manufacturers = data['manufacturers'] as List<String>? ?? [];
|
||||
equipmentNames = data['equipment_names'] as List<String>? ?? [];
|
||||
companies = data['companies'] as Map<int, String>? ?? {};
|
||||
warehouses = data['warehouses'] as Map<int, String>? ?? {};
|
||||
|
||||
// ✅ List<Map> → Map<int, String> 안전한 변환 (사전 로드된 데이터)
|
||||
try {
|
||||
final companiesList = data['companies'] as List<dynamic>? ?? [];
|
||||
companies = Map<int, String>.fromIterable(
|
||||
companiesList.where((item) => item != null && item['id'] != null && item['name'] != null),
|
||||
key: (item) => item['id'] as int,
|
||||
value: (item) => item['name'] as String,
|
||||
);
|
||||
|
||||
final warehousesList = data['warehouses'] as List<dynamic>? ?? [];
|
||||
warehouses = Map<int, String>.fromIterable(
|
||||
warehousesList.where((item) => item != null && item['id'] != null && item['name'] != null),
|
||||
key: (item) => item['id'] as int,
|
||||
value: (item) => item['name'] as String,
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
DebugLogger.logError('사전 로드된 드롭다운 데이터 변환 실패', error: e);
|
||||
companies = {};
|
||||
warehouses = {};
|
||||
}
|
||||
|
||||
DebugLogger.log('드롭다운 데이터 처리 완료', tag: 'EQUIPMENT_IN', data: {
|
||||
'manufacturers_count': manufacturers.length,
|
||||
@@ -255,7 +300,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 드롭다운 데이터 로드 (매번 API 호출)
|
||||
void _loadDropdownData() async {
|
||||
Future<void> _loadDropdownData() async {
|
||||
try {
|
||||
DebugLogger.log('Equipment 폼 드롭다운 데이터 로드 시작', tag: 'EQUIPMENT_IN');
|
||||
final result = await _lookupsService.getEquipmentFormDropdownData();
|
||||
@@ -268,13 +313,33 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
equipmentNames = [];
|
||||
companies = {};
|
||||
warehouses = {};
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
},
|
||||
(data) {
|
||||
manufacturers = data['manufacturers'] as List<String>;
|
||||
equipmentNames = data['equipment_names'] as List<String>;
|
||||
companies = data['companies'] as Map<int, String>;
|
||||
warehouses = data['warehouses'] as Map<int, String>;
|
||||
|
||||
// ✅ List<Map> → Map<int, String> 안전한 변환
|
||||
try {
|
||||
final companiesList = data['companies'] as List<dynamic>? ?? [];
|
||||
companies = Map<int, String>.fromIterable(
|
||||
companiesList.where((item) => item != null && item['id'] != null && item['name'] != null),
|
||||
key: (item) => item['id'] as int,
|
||||
value: (item) => item['name'] as String,
|
||||
);
|
||||
|
||||
final warehousesList = data['warehouses'] as List<dynamic>? ?? [];
|
||||
warehouses = Map<int, String>.fromIterable(
|
||||
warehousesList.where((item) => item != null && item['id'] != null && item['name'] != null),
|
||||
key: (item) => item['id'] as int,
|
||||
value: (item) => item['name'] as String,
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
DebugLogger.logError('드롭다운 데이터 변환 실패', error: e);
|
||||
companies = {};
|
||||
warehouses = {};
|
||||
}
|
||||
|
||||
DebugLogger.log('드롭다운 데이터 로드 성공', tag: 'EQUIPMENT_IN', data: {
|
||||
'manufacturers_count': manufacturers.length,
|
||||
@@ -283,7 +348,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
'warehouses_count': warehouses.length,
|
||||
});
|
||||
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -292,29 +357,45 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
equipmentNames = [];
|
||||
companies = {};
|
||||
warehouses = {};
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 기존의 개별 로드 메서드들은 _loadDropdownData()로 통합됨
|
||||
// warehouseLocations, partnerCompanies 리스트 변수들도 제거됨
|
||||
|
||||
// 전달받은 장비 데이터로 폼 초기화
|
||||
// 전달받은 장비 데이터로 폼 초기화 (간소화: 백엔드 JOIN 데이터 직접 활용)
|
||||
void _loadFromEquipment(EquipmentDto equipment) {
|
||||
serialNumber = equipment.serialNumber;
|
||||
barcode = equipment.barcode ?? '';
|
||||
modelsId = equipment.modelsId;
|
||||
// vendorId는 ModelDto에서 가져와야 함 (필요 시)
|
||||
purchasePrice = equipment.purchasePrice.toDouble();
|
||||
initialStock = 1; // EquipmentDto에는 initialStock 필드가 없음
|
||||
purchasePrice = equipment.purchasePrice > 0 ? equipment.purchasePrice.toDouble() : null;
|
||||
initialStock = 1;
|
||||
selectedCompanyId = equipment.companiesId;
|
||||
// selectedWarehouseId는 현재 위치를 추적해야 함 (EquipmentHistory에서)
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
warrantyNumberController.text = equipment.warrantyNumber;
|
||||
|
||||
// ✅ 간소화: 백엔드 JOIN 데이터 직접 사용 (복잡한 Controller 조회 제거)
|
||||
manufacturer = equipment.vendorName ?? '제조사 정보 없음';
|
||||
name = equipment.modelName ?? '모델 정보 없음';
|
||||
|
||||
// 날짜 필드 설정
|
||||
purchaseDate = equipment.purchasedAt;
|
||||
warrantyStartDate = equipment.warrantyStartedAt;
|
||||
warrantyEndDate = equipment.warrantyEndedAt;
|
||||
|
||||
// TextEditingController 동기화
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
warrantyNumberController.text = equipment.warrantyNumber;
|
||||
|
||||
// 수정 모드에서 입고지 기본값 설정
|
||||
if (isEditMode && selectedWarehouseId == null && warehouses.isNotEmpty) {
|
||||
selectedWarehouseId = warehouses.keys.first;
|
||||
}
|
||||
|
||||
// preloadedEquipment에 저장 (UI에서 JOIN 데이터 접근용)
|
||||
preloadedEquipment = equipment;
|
||||
|
||||
_updateCanSave();
|
||||
notifyListeners(); // UI 즉시 업데이트
|
||||
}
|
||||
|
||||
// 기존 데이터 로드(수정 모드)
|
||||
@@ -404,7 +485,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
_updateCanSave(); // 데이터 로드 완료 시 canSave 상태 업데이트
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +523,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
_isSaving = true;
|
||||
_error = null;
|
||||
_updateCanSave(); // 저장 시작 시 canSave 상태 업데이트
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
|
||||
try {
|
||||
|
||||
@@ -501,7 +582,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
companiesId: validCompanyId,
|
||||
modelsId: _modelsId,
|
||||
serialNumber: _serialNumber.trim(),
|
||||
barcode: null,
|
||||
barcode: _barcode.trim().isEmpty ? null : _barcode.trim(),
|
||||
purchasedAt: purchaseDate,
|
||||
purchasePrice: purchasePrice?.toInt(),
|
||||
warrantyNumber: validWarrantyNumber,
|
||||
@@ -538,17 +619,19 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
'companiesId': selectedCompanyId,
|
||||
});
|
||||
|
||||
// Equipment 객체를 EquipmentRequestDto로 변환
|
||||
// Equipment 객체를 EquipmentRequestDto로 변환 (백엔드 스펙에 맞게)
|
||||
final createRequest = EquipmentRequestDto(
|
||||
companiesId: selectedCompanyId ?? 0,
|
||||
modelsId: _modelsId ?? 0,
|
||||
companiesId: selectedCompanyId, // 백엔드: Option<i32> - null 허용
|
||||
modelsId: _modelsId, // 백엔드: Option<i32> - null 허용
|
||||
serialNumber: _serialNumber,
|
||||
barcode: null,
|
||||
purchasedAt: null,
|
||||
barcode: _barcode.trim().isEmpty ? null : _barcode.trim(),
|
||||
purchasedAt: (purchaseDate ?? DateTime.now()).toUtc(), // 단순 UTC 변환
|
||||
purchasePrice: purchasePrice?.toInt() ?? 0,
|
||||
warrantyNumber: '',
|
||||
warrantyStartedAt: DateTime.now(),
|
||||
warrantyEndedAt: DateTime.now().add(Duration(days: 365)),
|
||||
warrantyNumber: warrantyNumberController.text.isNotEmpty
|
||||
? warrantyNumberController.text
|
||||
: 'WR-${DateTime.now().millisecondsSinceEpoch}',
|
||||
warrantyStartedAt: warrantyStartDate.toUtc(), // 단순 UTC 변환
|
||||
warrantyEndedAt: warrantyEndDate.toUtc(), // 단순 UTC 변환
|
||||
remark: remarkController.text.isNotEmpty ? remarkController.text : null,
|
||||
);
|
||||
|
||||
@@ -602,21 +685,22 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
return true;
|
||||
} on Failure catch (e) {
|
||||
_error = e.message;
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
return false;
|
||||
} catch (e) {
|
||||
_error = 'An unexpected error occurred: $e';
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
return false;
|
||||
} finally {
|
||||
_isSaving = false;
|
||||
_updateCanSave(); // 저장 완료 시 canSave 상태 업데이트
|
||||
notifyListeners();
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 처리
|
||||
void clearError() {
|
||||
if (_disposed) return;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -625,6 +709,7 @@ class EquipmentInFormController extends ChangeNotifier {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true; // dispose 상태 설정
|
||||
remarkController.dispose();
|
||||
warrantyNumberController.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -10,6 +10,7 @@ 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';
|
||||
import 'package:superport/domain/usecases/equipment/search_equipment_usecase.dart';
|
||||
|
||||
/// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러 (리팩토링 버전)
|
||||
/// BaseListController를 상속받아 공통 기능을 재사용
|
||||
@@ -76,7 +77,7 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
status: _statusFilter != null ?
|
||||
EquipmentStatusConverter.clientToServer(_statusFilter) : null,
|
||||
search: params.search,
|
||||
// companyId: _companyIdFilter, // 비활성화: EquipmentService에서 지원하지 않음
|
||||
companyId: _companyIdFilter, // ✅ 활성화: 회사별 필터링 지원
|
||||
// includeInactive: _includeInactive, // 비활성화: EquipmentService에서 지원하지 않음
|
||||
),
|
||||
onError: (failure) {
|
||||
@@ -110,6 +111,15 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
equipmentNumber: dto.serialNumber ?? 'Unknown', // 장비번호 (required)
|
||||
serialNumber: dto.serialNumber ?? 'Unknown', // 시리얼번호 (required)
|
||||
quantity: 1, // 기본 수량
|
||||
// ⚡ [FIX] 누락된 구매 정보 필드들 추가
|
||||
purchasePrice: dto.purchasePrice.toDouble(), // int → double 변환
|
||||
purchaseDate: dto.purchasedAt, // 구매일
|
||||
barcode: dto.barcode, // 바코드
|
||||
remark: dto.remark, // 비고
|
||||
// 보증 정보
|
||||
warrantyLicense: dto.warrantyNumber,
|
||||
warrantyStartDate: dto.warrantyStartedAt,
|
||||
warrantyEndDate: dto.warrantyEndedAt,
|
||||
);
|
||||
|
||||
// 간단한 Company 정보 생성 (사용하지 않으므로 제거)
|
||||
@@ -129,6 +139,10 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
warehouseLocation: null, // EquipmentDto에 warehouse_name 필드 없음
|
||||
// currentBranch는 EquipmentListDto에 없으므로 null (백엔드 API 구조 변경으로 지점 개념 제거)
|
||||
currentBranch: null,
|
||||
// ⚡ [FIX] 백엔드 직접 제공 필드들 추가 - 화면에서 N/A 문제 해결
|
||||
companyName: dto.companyName, // API company_name → UI 회사명 컬럼
|
||||
vendorName: dto.vendorName, // API vendor_name → UI 제조사 컬럼
|
||||
modelName: dto.modelName, // API model_name → UI 모델명 컬럼
|
||||
);
|
||||
// 🔧 [DEBUG] 변환된 UnifiedEquipment 로깅 (필요 시 활성화)
|
||||
// print('DEBUG [EquipmentListController] UnifiedEquipment ID: ${unifiedEquipment.id}, currentCompany: "${unifiedEquipment.currentCompany}", warehouseLocation: "${unifiedEquipment.warehouseLocation}"');
|
||||
@@ -197,12 +211,34 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
try {
|
||||
final result = await _lookupsService.getEquipmentFormDropdownData();
|
||||
result.fold(
|
||||
(failure) => throw failure,
|
||||
(data) => cachedDropdownData = data,
|
||||
(failure) {
|
||||
debugPrint('❌ 드롭다운 데이터 로드 실패: ${failure.message}');
|
||||
// 실패해도 빈 데이터로 초기화하여 타입 오류 방지
|
||||
cachedDropdownData = {
|
||||
'manufacturers': <String>[],
|
||||
'equipment_names': <String>[],
|
||||
'companies': <Map<String, dynamic>>[],
|
||||
'warehouses': <Map<String, dynamic>>[],
|
||||
'category1_list': <String>[],
|
||||
'category_combinations': <dynamic>[],
|
||||
};
|
||||
},
|
||||
(data) {
|
||||
debugPrint('✅ 드롭다운 데이터 로드 성공: ${data.keys}');
|
||||
cachedDropdownData = data;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
print('Failed to preload dropdown data: $e');
|
||||
// 캐시 실패해도 계속 진행
|
||||
debugPrint('❌ 드롭다운 데이터 로드 예외: $e');
|
||||
// 예외 발생 시에도 빈 데이터로 초기화
|
||||
cachedDropdownData = {
|
||||
'manufacturers': <String>[],
|
||||
'equipment_names': <String>[],
|
||||
'companies': <Map<String, dynamic>>[],
|
||||
'warehouses': <Map<String, dynamic>>[],
|
||||
'category1_list': <String>[],
|
||||
'category_combinations': <dynamic>[],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +284,60 @@ class EquipmentListController extends BaseListController<UnifiedEquipment> {
|
||||
loadData(isRefresh: true);
|
||||
}
|
||||
|
||||
/// 시리얼번호로 장비 검색
|
||||
Future<EquipmentDto?> searchBySerial(String serial) async {
|
||||
try {
|
||||
final useCase = GetIt.instance<GetEquipmentBySerialUseCase>();
|
||||
final result = await useCase(serial);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
throw Exception(failure.message);
|
||||
},
|
||||
(equipment) => equipment,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('시리얼번호 검색 실패: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 바코드로 장비 검색
|
||||
Future<EquipmentDto?> searchByBarcode(String barcode) async {
|
||||
try {
|
||||
final useCase = GetIt.instance<GetEquipmentByBarcodeUseCase>();
|
||||
final result = await useCase(barcode);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
throw Exception(failure.message);
|
||||
},
|
||||
(equipment) => equipment,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('바코드 검색 실패: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사별 장비 목록 조회
|
||||
Future<List<EquipmentDto>?> getEquipmentsByCompany(int companyId) async {
|
||||
try {
|
||||
final useCase = GetIt.instance<GetEquipmentsByCompanyUseCase>();
|
||||
final result = await useCase(companyId);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
throw Exception(failure.message);
|
||||
},
|
||||
(equipments) => equipments,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('회사별 장비 조회 실패: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 필터 초기화
|
||||
void clearFilters() {
|
||||
_statusFilter = null;
|
||||
|
||||
@@ -21,6 +21,7 @@ class EquipmentInFormScreen extends StatefulWidget {
|
||||
class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
late EquipmentInFormController _controller;
|
||||
late TextEditingController _serialNumberController;
|
||||
late TextEditingController _barcodeController;
|
||||
late TextEditingController _initialStockController;
|
||||
late TextEditingController _purchasePriceController;
|
||||
Future<void>? _initFuture;
|
||||
@@ -49,6 +50,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
|
||||
// TextEditingController 초기화
|
||||
_serialNumberController = TextEditingController(text: _controller.serialNumber);
|
||||
_barcodeController = TextEditingController(text: _controller.barcode);
|
||||
_initialStockController = TextEditingController(text: _controller.initialStock.toString());
|
||||
_purchasePriceController = TextEditingController(
|
||||
text: _controller.purchasePrice != null
|
||||
@@ -62,6 +64,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
// 데이터 로드 후 컨트롤러 업데이트
|
||||
setState(() {
|
||||
_serialNumberController.text = _controller.serialNumber;
|
||||
_barcodeController.text = _controller.barcode;
|
||||
_purchasePriceController.text = _controller.purchasePrice != null
|
||||
? CurrencyFormatter.formatKRW(_controller.purchasePrice)
|
||||
: '';
|
||||
@@ -73,7 +76,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
_controller.removeListener(_onControllerUpdated);
|
||||
_controller.dispose();
|
||||
_serialNumberController.dispose();
|
||||
_serialNumberController.dispose();
|
||||
_barcodeController.dispose();
|
||||
_initialStockController.dispose();
|
||||
_purchasePriceController.dispose();
|
||||
super.dispose();
|
||||
@@ -167,6 +170,8 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
const SizedBox(height: 24),
|
||||
_buildPurchaseSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildWarrantySection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildRemarkSection(),
|
||||
],
|
||||
),
|
||||
@@ -183,6 +188,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'기본 정보',
|
||||
@@ -192,7 +198,22 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 장비 번호 (필수)
|
||||
// 1. 제조사/모델 정보 (수정 모드: 읽기 전용, 생성 모드: 선택)
|
||||
if (_controller.isEditMode)
|
||||
..._buildReadOnlyVendorModel()
|
||||
else
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: EquipmentVendorModelSelector(
|
||||
initialVendorId: _controller.vendorId,
|
||||
initialModelId: _controller.modelsId,
|
||||
onChanged: _controller.onVendorModelChanged,
|
||||
isReadOnly: false,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. 장비 번호 (필수)
|
||||
ShadInputFormField(
|
||||
controller: _serialNumberController,
|
||||
readOnly: _controller.isFieldReadOnly('serialNumber'),
|
||||
@@ -202,40 +223,27 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
label: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '장비 번호 * 🔒' : '장비 번호 *'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty ?? true) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return '장비 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: _controller.isFieldReadOnly('serialNumber') ? null : (value) {
|
||||
_controller.serialNumber = value.trim() ?? '';
|
||||
_controller.serialNumber = value?.trim() ?? '';
|
||||
setState(() {});
|
||||
print('DEBUG [장비번호 입력] value: "$value", controller.serialNumber: "${_controller.serialNumber}"');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Vendor→Model cascade 선택기
|
||||
EquipmentVendorModelSelector(
|
||||
initialVendorId: _controller.vendorId,
|
||||
initialModelId: _controller.modelsId,
|
||||
onChanged: _controller.onVendorModelChanged,
|
||||
isReadOnly: _controller.isFieldReadOnly('modelsId'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 시리얼 번호 (선택)
|
||||
// 3. 바코드 (선택사항)
|
||||
ShadInputFormField(
|
||||
controller: _serialNumberController,
|
||||
readOnly: _controller.isFieldReadOnly('serialNumber'),
|
||||
placeholder: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '수정불가' : '시리얼 번호를 입력하세요'),
|
||||
label: Text(_controller.isFieldReadOnly('serialNumber')
|
||||
? '시리얼 번호 🔒' : '시리얼 번호'),
|
||||
onChanged: _controller.isFieldReadOnly('serialNumber') ? null : (value) {
|
||||
_controller.serialNumber = value.trim() ?? '';
|
||||
setState(() {});
|
||||
print('DEBUG [시리얼번호 입력] value: "$value", controller.serialNumber: "${_controller.serialNumber}"');
|
||||
controller: _barcodeController,
|
||||
placeholder: const Text('바코드를 입력하세요'),
|
||||
label: const Text('바코드'),
|
||||
onChanged: (value) {
|
||||
_controller.barcode = value?.trim() ?? '';
|
||||
print('DEBUG [바코드 입력] value: "$value", controller.barcode: "${_controller.barcode}"');
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -260,30 +268,32 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 구매처 (드롭다운 전용)
|
||||
ShadSelect<int>(
|
||||
initialValue: _getValidCompanyId(),
|
||||
placeholder: const Text('구매처를 선택하세요'),
|
||||
options: _controller.companies.entries.map((entry) =>
|
||||
ShadOption(
|
||||
value: entry.key,
|
||||
child: Text(entry.value),
|
||||
)
|
||||
).toList(),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
// companies가 비어있거나 해당 value가 없는 경우 처리
|
||||
if (_controller.companies.isEmpty) {
|
||||
return const Text('로딩중...');
|
||||
}
|
||||
return Text(_controller.companies[value] ?? '선택하세요');
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedCompanyId = value;
|
||||
});
|
||||
print('DEBUG [구매처 선택] value: $value, companies: ${_controller.companies.length}');
|
||||
},
|
||||
),
|
||||
// 구매처 (수정 모드: 읽기 전용, 생성 모드: 선택)
|
||||
if (_controller.isEditMode)
|
||||
_buildReadOnlyCompany()
|
||||
else
|
||||
ShadSelect<int>(
|
||||
initialValue: _getValidCompanyId(),
|
||||
placeholder: const Text('구매처를 선택하세요'),
|
||||
options: _controller.companies.entries.map((entry) =>
|
||||
ShadOption(
|
||||
value: entry.key,
|
||||
child: Text(entry.value),
|
||||
)
|
||||
).toList(),
|
||||
selectedOptionBuilder: (context, value) {
|
||||
if (_controller.companies.isEmpty) {
|
||||
return const Text('로딩중...');
|
||||
}
|
||||
return Text(_controller.companies[value] ?? '선택하세요');
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedCompanyId = value;
|
||||
});
|
||||
print('DEBUG [구매처 선택] value: $value, companies: ${_controller.companies.length}');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 입고지 (드롭다운 전용)
|
||||
@@ -396,7 +406,7 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
Text(
|
||||
_controller.purchaseDate != null
|
||||
? '${_controller.purchaseDate!.year}-${_controller.purchaseDate!.month.toString().padLeft(2, '0')}-${_controller.purchaseDate!.day.toString().padLeft(2, '0')}'
|
||||
: _controller.isFieldReadOnly('purchaseDate') ? '구매일 미설정' : '날짜 선택',
|
||||
: _controller.isFieldReadOnly('purchaseDate') ? '구매일 미설정' : '현재 날짜',
|
||||
style: TextStyle(
|
||||
color: _controller.isFieldReadOnly('purchaseDate')
|
||||
? Colors.grey[600]
|
||||
@@ -444,6 +454,140 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWarrantySection() {
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'워런티 정보 *',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 워런티 번호 (필수)
|
||||
ShadInputFormField(
|
||||
controller: _controller.warrantyNumberController,
|
||||
label: const Text('워런티 번호 *'),
|
||||
placeholder: const Text('워런티 번호를 입력하세요'),
|
||||
validator: (value) {
|
||||
if (value.trim().isEmpty ?? true) {
|
||||
return '워런티 번호는 필수입니다';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
// 워런티 시작일 (필수)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.warrantyStartDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_controller.warrantyStartDate = date;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_controller.warrantyStartDate.year}-${_controller.warrantyStartDate.month.toString().padLeft(2, '0')}-${_controller.warrantyStartDate.day.toString().padLeft(2, '0')}',
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 워런티 만료일 (필수)
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _controller.warrantyEndDate,
|
||||
firstDate: _controller.warrantyStartDate,
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_controller.warrantyEndDate = date;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${_controller.warrantyEndDate.year}-${_controller.warrantyEndDate.month.toString().padLeft(2, '0')}-${_controller.warrantyEndDate.day.toString().padLeft(2, '0')}',
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 워런티 기간 표시
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'워런티 기간: ${_controller.getWarrantyPeriodSummary()}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRemarkSection() {
|
||||
return ShadCard(
|
||||
child: Padding(
|
||||
@@ -471,4 +615,43 @@ class _EquipmentInFormScreenState extends State<EquipmentInFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 읽기 전용 구매처 정보 표시 (백엔드 JOIN 데이터 활용)
|
||||
Widget _buildReadOnlyCompany() {
|
||||
// preloadedEquipment가 있으면 companyName 사용, 없으면 기본값
|
||||
final companyName = _controller.preloadedEquipment?.companyName ??
|
||||
(_controller.companies.isNotEmpty && _controller.selectedCompanyId != null
|
||||
? _controller.companies[_controller.selectedCompanyId]
|
||||
: '구매처 정보 없음');
|
||||
|
||||
return ShadInputFormField(
|
||||
readOnly: true,
|
||||
label: const Text('구매처 🔒'),
|
||||
initialValue: companyName,
|
||||
);
|
||||
}
|
||||
|
||||
/// 읽기 전용 제조사/모델 정보 표시 (백엔드 JOIN 데이터 활용)
|
||||
List<Widget> _buildReadOnlyVendorModel() {
|
||||
return [
|
||||
// 제조사 (읽기 전용)
|
||||
ShadInputFormField(
|
||||
readOnly: true,
|
||||
label: const Text('제조사 * 🔒'),
|
||||
initialValue: _controller.manufacturer.isNotEmpty
|
||||
? _controller.manufacturer
|
||||
: '제조사 정보 없음',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 모델 (읽기 전용)
|
||||
ShadInputFormField(
|
||||
readOnly: true,
|
||||
label: const Text('모델 * 🔒'),
|
||||
initialValue: _controller.name.isNotEmpty
|
||||
? _controller.name
|
||||
: '모델 정보 없음',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import 'package:superport/screens/common/widgets/standard_states.dart';
|
||||
import 'package:superport/screens/common/layouts/base_list_screen.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_history_dialog.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_search_dialog.dart';
|
||||
|
||||
/// shadcn/ui 스타일로 재설계된 장비 관리 화면
|
||||
class EquipmentList extends StatefulWidget {
|
||||
@@ -38,7 +40,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentListController();
|
||||
_controller.pageSize = 10; // 페이지 크기 설정
|
||||
_controller.pageSize = AppConstants.equipmentPageSize; // 페이지 크기 설정
|
||||
_setInitialFilter();
|
||||
_preloadDropdownData(); // 드롭다운 데이터 미리 로드
|
||||
|
||||
@@ -76,11 +78,12 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
_adjustColumnsForScreenSize();
|
||||
}
|
||||
|
||||
/// 화면 크기에 따라 컬럼 표시 조정
|
||||
/// 화면 크기에 따라 컬럼 표시 조정 - 다단계 반응형
|
||||
void _adjustColumnsForScreenSize() {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
setState(() {
|
||||
_showDetailedColumns = width > 900;
|
||||
// 1200px 이상에서만 상세 컬럼 (바코드, 구매가격, 구매일, 보증기간) 표시
|
||||
_showDetailedColumns = width > 1200;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,6 +148,14 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
_controller.changeStatusFilter(_controller.selectedStatusFilter);
|
||||
}
|
||||
|
||||
/// 회사 필터 변경
|
||||
Future<void> _onCompanyFilterChanged(int? companyId) async {
|
||||
setState(() {
|
||||
_controller.filterByCompany(companyId);
|
||||
_controller.goToPage(1);
|
||||
});
|
||||
}
|
||||
|
||||
/// 검색 실행
|
||||
void _onSearch() async {
|
||||
setState(() {
|
||||
@@ -185,10 +196,10 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
equipments = equipments.where((e) {
|
||||
final keyword = _appliedSearchKeyword.toLowerCase();
|
||||
return [
|
||||
e.equipment.model?.vendor?.name ?? '', // Vendor 이름
|
||||
e.equipment.serialNumber ?? '', // 시리얼 번호 (메인 필드)
|
||||
e.equipment.model?.name ?? '', // Model 이름
|
||||
e.equipment.serialNumber ?? '', // 시리얼 번호 (중복 제거)
|
||||
e.vendorName ?? '', // 백엔드 직접 제공 Vendor 이름
|
||||
e.modelName ?? '', // 백엔드 직접 제공 Model 이름
|
||||
e.companyName ?? '', // 백엔드 직접 제공 Company 이름
|
||||
e.equipment.serialNumber ?? '', // 시리얼 번호
|
||||
e.equipment.barcode ?? '', // 바코드
|
||||
e.equipment.remark ?? '', // 비고
|
||||
].any((field) => field.toLowerCase().contains(keyword.toLowerCase()));
|
||||
@@ -285,7 +296,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
'${equipment.model?.vendor?.name ?? 'N/A'} ${equipment.serialNumber}', // Vendor + Equipment Number
|
||||
'${unifiedEquipment.vendorName ?? 'N/A'} ${equipment.serialNumber}', // 백엔드 직접 제공 Vendor + Equipment Number
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
);
|
||||
@@ -523,6 +534,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final filteredEquipments = _getFilteredEquipments();
|
||||
// 백엔드 API에서 제공하는 실제 전체 아이템 수 사용
|
||||
final totalCount = controller.total;
|
||||
|
||||
// 디버그: 페이지네이션 상태 확인
|
||||
print('DEBUG Pagination: total=${controller.total}, totalPages=${controller.totalPages}, pageSize=${controller.pageSize}, currentPage=${controller.currentPage}');
|
||||
|
||||
return BaseListScreen(
|
||||
isLoading: controller.isLoading && controller.equipments.isEmpty,
|
||||
@@ -543,8 +557,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
// 데이터 테이블
|
||||
dataTable: _buildDataTable(filteredEquipments),
|
||||
|
||||
// 페이지네이션
|
||||
pagination: controller.totalPages > 1 ? Pagination(
|
||||
// 페이지네이션 - 조건 수정으로 표시 개선
|
||||
pagination: controller.total > controller.pageSize ? Pagination(
|
||||
totalCount: controller.total,
|
||||
currentPage: controller.currentPage,
|
||||
pageSize: controller.pageSize,
|
||||
@@ -621,6 +635,25 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 회사별 필터 드롭다운
|
||||
SizedBox(
|
||||
height: 40,
|
||||
width: 150,
|
||||
child: ShadSelect<int?>(
|
||||
selectedOptionBuilder: (context, value) => Text(
|
||||
value == null ? '전체 회사' : _getCompanyDisplayText(value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
placeholder: const Text('회사 선택'),
|
||||
options: _buildCompanySelectOptions(),
|
||||
onChanged: (value) {
|
||||
_onCompanyFilterChanged(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -631,6 +664,19 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
leftActions: [
|
||||
// 라우트별 액션 버튼
|
||||
_buildRouteSpecificActions(selectedInCount, selectedOutCount, selectedRentCount),
|
||||
const SizedBox(width: 8),
|
||||
// 검색 버튼 추가
|
||||
ShadButton.outline(
|
||||
onPressed: () => _showEquipmentSearchDialog(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.search, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
const Text('고급 검색'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
rightActions: [
|
||||
// 관리자용 비활성 포함 체크박스
|
||||
@@ -667,7 +713,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
Widget _buildRouteSpecificActions(int selectedInCount, int selectedOutCount, int selectedRentCount) {
|
||||
switch (widget.currentRoute) {
|
||||
case Routes.equipmentInList:
|
||||
return Row(
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '출고',
|
||||
@@ -675,7 +723,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
variant: selectedInCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.exit_to_app, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadcnButton(
|
||||
text: '입고',
|
||||
onPressed: () async {
|
||||
@@ -695,7 +742,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
],
|
||||
);
|
||||
case Routes.equipmentOutList:
|
||||
return Row(
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '재입고',
|
||||
@@ -710,9 +759,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
variant: selectedOutCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.assignment_return, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadcnButton(
|
||||
text: '수리 요청',
|
||||
text: '수리',
|
||||
onPressed: selectedOutCount > 0
|
||||
? () => ShadToaster.of(context).show(
|
||||
const ShadToast(
|
||||
@@ -727,7 +775,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
],
|
||||
);
|
||||
case Routes.equipmentRentList:
|
||||
return Row(
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '반납',
|
||||
@@ -742,7 +792,6 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
variant: selectedRentCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.keyboard_return, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadcnButton(
|
||||
text: '연장',
|
||||
onPressed: selectedRentCount > 0
|
||||
@@ -759,7 +808,9 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
],
|
||||
);
|
||||
default:
|
||||
return Row(
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
ShadcnButton(
|
||||
text: '입고',
|
||||
@@ -777,24 +828,21 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
textColor: Colors.white,
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadcnButton(
|
||||
text: '출고 처리',
|
||||
text: '출고',
|
||||
onPressed: selectedInCount > 0 ? _handleOutEquipment : null,
|
||||
variant: selectedInCount > 0 ? ShadcnButtonVariant.primary : ShadcnButtonVariant.secondary,
|
||||
textColor: selectedInCount > 0 ? Colors.white : null,
|
||||
icon: const Icon(Icons.local_shipping, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadcnButton(
|
||||
text: '대여 처리',
|
||||
text: '대여',
|
||||
onPressed: selectedInCount > 0 ? _handleRentEquipment : null,
|
||||
variant: selectedInCount > 0 ? ShadcnButtonVariant.secondary : ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.assignment, size: 16),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadcnButton(
|
||||
text: '폐기 처리',
|
||||
text: '폐기',
|
||||
onPressed: selectedInCount > 0 ? _handleDisposeEquipment : null,
|
||||
variant: selectedInCount > 0 ? ShadcnButtonVariant.destructive : ShadcnButtonVariant.secondary,
|
||||
icon: const Icon(Icons.delete, size: 16),
|
||||
@@ -805,28 +853,36 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
|
||||
/// 최소 테이블 너비 계산
|
||||
double _getMinimumTableWidth(List<UnifiedEquipment> pagedEquipments) {
|
||||
/// 최소 테이블 너비 계산 - 반응형 최적화
|
||||
double _getMinimumTableWidth(List<UnifiedEquipment> pagedEquipments, double availableWidth) {
|
||||
double totalWidth = 0;
|
||||
|
||||
// 기본 컬럼들 (리스트 API에서 제공하는 데이터만)
|
||||
totalWidth += 40; // 체크박스
|
||||
totalWidth += 50; // 번호
|
||||
totalWidth += 120; // 제조사
|
||||
totalWidth += 120; // 장비번호
|
||||
totalWidth += 120; // 모델명
|
||||
totalWidth += 50; // 수량
|
||||
totalWidth += 70; // 상태
|
||||
totalWidth += 80; // 입출고일
|
||||
totalWidth += 90; // 관리
|
||||
// 필수 컬럼들 (항상 표시) - 더 작게 조정
|
||||
totalWidth += 30; // 체크박스 (35->30)
|
||||
totalWidth += 35; // 번호 (40->35)
|
||||
totalWidth += 70; // 회사명 (90->70)
|
||||
totalWidth += 60; // 제조사 (80->60)
|
||||
totalWidth += 80; // 모델명 (100->80)
|
||||
totalWidth += 70; // 장비번호 (90->70)
|
||||
totalWidth += 50; // 상태 (60->50)
|
||||
totalWidth += 90; // 관리 (120->90, 아이콘 전용으로 최적화)
|
||||
|
||||
// 상세 컬럼들 (조건부)
|
||||
if (_showDetailedColumns) {
|
||||
totalWidth += 120; // 시리얼번호
|
||||
// 중간 화면용 추가 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) {
|
||||
totalWidth += 35; // 수량 (40->35)
|
||||
totalWidth += 70; // 입출고일 (80->70)
|
||||
}
|
||||
|
||||
// padding 추가 (좌우 각 16px)
|
||||
totalWidth += 32;
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) {
|
||||
totalWidth += 70; // 바코드 (90->70)
|
||||
totalWidth += 70; // 구매가격 (80->70)
|
||||
totalWidth += 70; // 구매일 (80->70)
|
||||
totalWidth += 80; // 보증기간 (90->80)
|
||||
}
|
||||
|
||||
// padding 추가 (좌우 각 2px로 축소)
|
||||
totalWidth += 4;
|
||||
|
||||
return totalWidth;
|
||||
}
|
||||
@@ -873,16 +929,16 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
}
|
||||
|
||||
/// 유연한 테이블 빌더 - Virtual Scrolling 적용
|
||||
Widget _buildFlexibleTable(List<UnifiedEquipment> pagedEquipments, {required bool useExpanded}) {
|
||||
Widget _buildFlexibleTable(List<UnifiedEquipment> pagedEquipments, {required bool useExpanded, required double availableWidth}) {
|
||||
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,
|
||||
horizontal: ShadcnTheme.spacing1, // spacing2 -> spacing1로 더 축소
|
||||
vertical: 6, // 8 -> 6으로 더 축소
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ShadcnTheme.muted.withValues(alpha: 0.3),
|
||||
@@ -892,6 +948,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 필수 컬럼들 (항상 표시) - 축소된 너비 적용
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
@@ -900,30 +957,38 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 40,
|
||||
minWidth: 30,
|
||||
),
|
||||
// 번호
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 50),
|
||||
_buildHeaderCell('번호', flex: 1, useExpanded: useExpanded, minWidth: 35),
|
||||
// 회사명 (소유회사)
|
||||
_buildHeaderCell('소유회사', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 제조사
|
||||
_buildHeaderCell('제조사', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
// 장비번호
|
||||
_buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 120),
|
||||
_buildHeaderCell('제조사', flex: 2, useExpanded: useExpanded, minWidth: 60),
|
||||
// 모델명
|
||||
_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: 3, useExpanded: useExpanded, minWidth: 80),
|
||||
// 장비번호
|
||||
_buildHeaderCell('장비번호', flex: 3, useExpanded: useExpanded, minWidth: 70),
|
||||
// 상태
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
// 입출고일
|
||||
_buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
_buildHeaderCell('상태', flex: 2, useExpanded: useExpanded, minWidth: 50),
|
||||
// 관리
|
||||
_buildHeaderCell('관리', flex: 2, useExpanded: useExpanded, minWidth: 90),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
// 수량
|
||||
_buildHeaderCell('수량', flex: 1, useExpanded: useExpanded, minWidth: 35),
|
||||
// 입출고일
|
||||
_buildHeaderCell('입출고일', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
],
|
||||
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) ...[
|
||||
_buildHeaderCell('바코드', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('구매가격', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('구매일', flex: 2, useExpanded: useExpanded, minWidth: 70),
|
||||
_buildHeaderCell('보증기간', flex: 2, useExpanded: useExpanded, minWidth: 80),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -959,8 +1024,8 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: ShadcnTheme.spacing4,
|
||||
vertical: 4,
|
||||
horizontal: ShadcnTheme.spacing1, // spacing2 -> spacing1로 더 축소
|
||||
vertical: 2, // 3 -> 2로 더 축소
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
@@ -969,6 +1034,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 필수 컬럼들 (항상 표시) - 축소된 너비 적용
|
||||
// 체크박스
|
||||
_buildDataCell(
|
||||
ShadCheckbox(
|
||||
@@ -981,7 +1047,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 40,
|
||||
minWidth: 30,
|
||||
),
|
||||
// 번호
|
||||
_buildDataCell(
|
||||
@@ -991,17 +1057,37 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 50,
|
||||
minWidth: 35,
|
||||
),
|
||||
// 소유회사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.companyName ?? 'N/A',
|
||||
equipment.companyName ?? 'N/A',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 제조사
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.model?.vendor?.name ?? 'N/A',
|
||||
equipment.equipment.model?.vendor?.name ?? 'N/A',
|
||||
equipment.vendorName ?? 'N/A',
|
||||
equipment.vendorName ?? 'N/A',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 60,
|
||||
),
|
||||
// 모델명
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.modelName ?? '-',
|
||||
equipment.modelName ?? '-',
|
||||
),
|
||||
flex: 3,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 120,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 장비번호
|
||||
_buildDataCell(
|
||||
@@ -1011,68 +1097,120 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
),
|
||||
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,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 상태
|
||||
_buildDataCell(
|
||||
_buildStatusBadge(equipment.status),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
minWidth: 50,
|
||||
),
|
||||
// 입출고일
|
||||
// 관리 (아이콘 전용 버튼으로 최적화)
|
||||
_buildDataCell(
|
||||
_buildCreatedDateWidget(equipment),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
// 관리
|
||||
_buildDataCell(
|
||||
_buildActionButtons(equipment.equipment.id ?? 0),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Tooltip(
|
||||
message: '이력 보기',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipment.equipment.id ?? 0),
|
||||
child: const Icon(Icons.history, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Tooltip(
|
||||
message: '수정',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEdit(equipment),
|
||||
child: const Icon(Icons.edit, size: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Tooltip(
|
||||
message: '삭제',
|
||||
child: ShadButton.ghost(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDelete(equipment),
|
||||
child: const Icon(Icons.delete_outline, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 90,
|
||||
),
|
||||
|
||||
// 중간 화면용 컬럼들 (800px 이상)
|
||||
if (availableWidth > 800) ...[
|
||||
// 수량 (백엔드에서 관리하지 않으므로 고정값)
|
||||
_buildDataCell(
|
||||
Text(
|
||||
'1',
|
||||
style: ShadcnTheme.bodySmall,
|
||||
),
|
||||
flex: 1,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 35,
|
||||
),
|
||||
// 입출고일
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatDate(equipment.date),
|
||||
_formatDate(equipment.date),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
],
|
||||
|
||||
// 상세 컬럼들 (1200px 이상에서만 표시)
|
||||
if (_showDetailedColumns && availableWidth > 1200) ...[
|
||||
// 바코드
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
equipment.equipment.barcode ?? '-',
|
||||
equipment.equipment.barcode ?? '-',
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 구매가격
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatPrice(equipment.equipment.purchasePrice),
|
||||
_formatPrice(equipment.equipment.purchasePrice),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 구매일
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatDate(equipment.equipment.purchaseDate),
|
||||
_formatDate(equipment.equipment.purchaseDate),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 70,
|
||||
),
|
||||
// 보증기간
|
||||
_buildDataCell(
|
||||
_buildTextWithTooltip(
|
||||
_formatWarrantyPeriod(equipment.equipment.warrantyStartDate, equipment.equipment.warrantyEndDate),
|
||||
_formatWarrantyPeriod(equipment.equipment.warrantyStartDate, equipment.equipment.warrantyEndDate),
|
||||
),
|
||||
flex: 2,
|
||||
useExpanded: useExpanded,
|
||||
minWidth: 80,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1127,7 +1265,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableWidth = constraints.maxWidth;
|
||||
final minimumWidth = _getMinimumTableWidth(pagedEquipments);
|
||||
final minimumWidth = _getMinimumTableWidth(pagedEquipments, availableWidth);
|
||||
final needsHorizontalScroll = minimumWidth > availableWidth;
|
||||
|
||||
if (needsHorizontalScroll) {
|
||||
@@ -1137,12 +1275,12 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
controller: _horizontalScrollController,
|
||||
child: SizedBox(
|
||||
width: minimumWidth,
|
||||
child: _buildFlexibleTable(pagedEquipments, useExpanded: false),
|
||||
child: _buildFlexibleTable(pagedEquipments, useExpanded: false, availableWidth: availableWidth),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 충분한 공간이 있을 때는 Expanded 사용
|
||||
return _buildFlexibleTable(pagedEquipments, useExpanded: true);
|
||||
return _buildFlexibleTable(pagedEquipments, useExpanded: true, availableWidth: availableWidth);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1161,6 +1299,35 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 가격 포맷팅
|
||||
String _formatPrice(double? price) {
|
||||
if (price == null) return '-';
|
||||
return '${(price / 10000).toStringAsFixed(0)}만원';
|
||||
}
|
||||
|
||||
/// 날짜 포맷팅
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '-';
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 보증기간 포맷팅
|
||||
String _formatWarrantyPeriod(DateTime? startDate, DateTime? endDate) {
|
||||
if (startDate == null || endDate == null) return '-';
|
||||
|
||||
final now = DateTime.now();
|
||||
final isExpired = now.isAfter(endDate);
|
||||
final remainingDays = isExpired ? 0 : endDate.difference(now).inDays;
|
||||
|
||||
if (isExpired) {
|
||||
return '만료됨';
|
||||
} else if (remainingDays <= 30) {
|
||||
return '$remainingDays일 남음';
|
||||
} else {
|
||||
return _formatDate(endDate);
|
||||
}
|
||||
}
|
||||
|
||||
/// 재고 상태 위젯 빌더 (백엔드 기반 단순화)
|
||||
Widget _buildInventoryStatus(UnifiedEquipment equipment) {
|
||||
// 백엔드 Equipment_History 기반으로 단순 상태만 표시
|
||||
@@ -1260,41 +1427,32 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: IconButton(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 30,
|
||||
minHeight: 30,
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.history, size: 16),
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipmentId),
|
||||
tooltip: '이력',
|
||||
// 이력 버튼 - 텍스트 + 아이콘으로 강화
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _showEquipmentHistoryDialog(equipmentId),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.history, size: 14),
|
||||
SizedBox(width: 4),
|
||||
Text('이력', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: IconButton(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 30,
|
||||
minHeight: 30,
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
onPressed: () => _handleEditById(equipmentId),
|
||||
tooltip: '편집',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 편집 버튼
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleEditById(equipmentId),
|
||||
child: const Icon(Icons.edit_outlined, size: 14),
|
||||
),
|
||||
Flexible(
|
||||
child: IconButton(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 30,
|
||||
minHeight: 30,
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
onPressed: () => _handleDeleteById(equipmentId),
|
||||
tooltip: '삭제',
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 삭제 버튼
|
||||
ShadButton.outline(
|
||||
size: ShadButtonSize.sm,
|
||||
onPressed: () => _handleDeleteById(equipmentId),
|
||||
child: const Icon(Icons.delete_outline, size: 14),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -1312,7 +1470,7 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
final result = await EquipmentHistoryDialog.show(
|
||||
context: context,
|
||||
equipmentId: equipmentId,
|
||||
equipmentName: '${equipment.equipment.model?.vendor?.name ?? 'N/A'} ${equipment.equipment.serialNumber}', // Vendor + Equipment Number
|
||||
equipmentName: '${equipment.vendorName ?? 'N/A'} ${equipment.equipment.serialNumber}', // 백엔드 직접 제공 Vendor + Equipment Number
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
@@ -1413,5 +1571,69 @@ class _EquipmentListState extends State<EquipmentList> {
|
||||
return options;
|
||||
}
|
||||
|
||||
/// 회사명 표시 텍스트 가져오기
|
||||
String _getCompanyDisplayText(int companyId) {
|
||||
// 캐시된 드롭다운 데이터에서 회사명 찾기
|
||||
if (_cachedDropdownData != null && _cachedDropdownData!['companies'] != null) {
|
||||
final companies = _cachedDropdownData!['companies'] as List<dynamic>;
|
||||
for (final company in companies) {
|
||||
if (company['id'] == companyId) {
|
||||
return company['name'] ?? '알수없는 회사';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '회사 #$companyId';
|
||||
}
|
||||
|
||||
/// 소유회사별 필터 드롭다운 옵션 생성
|
||||
List<ShadOption<int?>> _buildCompanySelectOptions() {
|
||||
List<ShadOption<int?>> options = [
|
||||
const ShadOption(value: null, child: Text('전체 소유회사')),
|
||||
];
|
||||
|
||||
// 캐시된 드롭다운 데이터에서 회사 목록 가져오기
|
||||
if (_cachedDropdownData != null && _cachedDropdownData!['companies'] != null) {
|
||||
final companies = _cachedDropdownData!['companies'] as List<dynamic>;
|
||||
|
||||
for (final company in companies) {
|
||||
final id = company['id'] as int?;
|
||||
final name = company['name'] as String?;
|
||||
|
||||
if (id != null && name != null) {
|
||||
options.add(
|
||||
ShadOption(
|
||||
value: id,
|
||||
child: Text(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// 사용하지 않는 현재위치, 점검일 관련 함수들 제거됨 (리스트 API에서 제공하지 않음)
|
||||
|
||||
/// 장비 고급 검색 다이얼로그 표시
|
||||
void _showEquipmentSearchDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => EquipmentSearchDialog(
|
||||
onEquipmentFound: (equipment) {
|
||||
// 검색된 장비를 상세보기로 이동 또는 다른 처리
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('장비 검색 완료'),
|
||||
description: Text('${equipment.serialNumber} 장비를 찾았습니다.'),
|
||||
),
|
||||
);
|
||||
// 필요하면 검색된 장비의 상세정보로 이동
|
||||
// _onEditTap(equipment);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:superport/core/constants/app_constants.dart';
|
||||
import 'package:superport/data/models/equipment_history_dto.dart';
|
||||
import 'package:superport/services/equipment_service.dart';
|
||||
import 'package:superport/core/errors/failures.dart';
|
||||
@@ -50,7 +51,7 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
||||
bool _isInitialLoad = true;
|
||||
String? _error;
|
||||
int _currentPage = 1;
|
||||
final int _perPage = 20;
|
||||
final int _perPage = AppConstants.historyPageSize;
|
||||
bool _hasMore = true;
|
||||
String _searchQuery = '';
|
||||
|
||||
@@ -339,11 +340,11 @@ class _EquipmentHistoryDialogState extends State<EquipmentHistoryDialog> {
|
||||
horizontal: isDesktop ? 40 : 10,
|
||||
vertical: isDesktop ? 40 : 20,
|
||||
),
|
||||
child: RawKeyboardListener(
|
||||
child: KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
autofocus: true,
|
||||
onKey: (RawKeyEvent event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
onKeyEvent: (KeyEvent event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
210
lib/screens/equipment/widgets/equipment_restore_dialog.dart
Normal file
210
lib/screens/equipment/widgets/equipment_restore_dialog.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../common/theme_shadcn.dart';
|
||||
import '../../../data/models/equipment/equipment_dto.dart';
|
||||
import '../../../injection_container.dart';
|
||||
import '../controllers/equipment_controller.dart';
|
||||
|
||||
/// 장비 복구 확인 다이얼로그
|
||||
class EquipmentRestoreDialog extends StatefulWidget {
|
||||
final EquipmentDto equipment;
|
||||
final VoidCallback? onRestored;
|
||||
|
||||
const EquipmentRestoreDialog({
|
||||
super.key,
|
||||
required this.equipment,
|
||||
this.onRestored,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EquipmentRestoreDialog> createState() => _EquipmentRestoreDialogState();
|
||||
}
|
||||
|
||||
class _EquipmentRestoreDialogState extends State<EquipmentRestoreDialog> {
|
||||
late final EquipmentController _controller;
|
||||
bool _isRestoring = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = sl<EquipmentController>();
|
||||
}
|
||||
|
||||
Future<void> _restore() async {
|
||||
setState(() {
|
||||
_isRestoring = true;
|
||||
});
|
||||
|
||||
final success = await _controller.restoreEquipment(widget.equipment.id);
|
||||
|
||||
if (mounted) {
|
||||
if (success) {
|
||||
Navigator.of(context).pop(true);
|
||||
if (widget.onRestored != null) {
|
||||
widget.onRestored!();
|
||||
}
|
||||
|
||||
// 성공 메시지
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast(
|
||||
title: const Text('복구 완료'),
|
||||
description: Text('${widget.equipment.serialNumber} 장비가 복구되었습니다.'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_isRestoring = false;
|
||||
});
|
||||
|
||||
// 실패 메시지
|
||||
ShadToaster.of(context).show(
|
||||
ShadToast.destructive(
|
||||
title: const Text('복구 실패'),
|
||||
description: Text(_controller.error ?? '장비 복구에 실패했습니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadDialog(
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.restore, color: Colors.green, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('장비 복구', style: ShadcnTheme.headingH3),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 복구 정보
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'다음 장비를 복구하시겠습니까?',
|
||||
style: ShadcnTheme.bodyLarge.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('시리얼 번호', widget.equipment.serialNumber),
|
||||
if (widget.equipment.barcode != null)
|
||||
_buildInfoRow('바코드', widget.equipment.barcode!),
|
||||
if (widget.equipment.modelName != null)
|
||||
_buildInfoRow('모델명', widget.equipment.modelName!),
|
||||
if (widget.equipment.companyName != null)
|
||||
_buildInfoRow('소속 회사', widget.equipment.companyName!),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
'복구된 장비는 다시 활성 상태로 변경됩니다.',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.foregroundMuted,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 버튼들
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: _isRestoring ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadButton(
|
||||
onPressed: _isRestoring ? null : _restore,
|
||||
child: _isRestoring
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('복구 중...'),
|
||||
],
|
||||
)
|
||||
: const Text('복구'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.foregroundMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 복구 다이얼로그 표시 유틸리티
|
||||
Future<bool?> showEquipmentRestoreDialog(
|
||||
BuildContext context, {
|
||||
required EquipmentDto equipment,
|
||||
VoidCallback? onRestored,
|
||||
}) async {
|
||||
return await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => EquipmentRestoreDialog(
|
||||
equipment: equipment,
|
||||
onRestored: onRestored,
|
||||
),
|
||||
);
|
||||
}
|
||||
374
lib/screens/equipment/widgets/equipment_search_dialog.dart
Normal file
374
lib/screens/equipment/widgets/equipment_search_dialog.dart
Normal file
@@ -0,0 +1,374 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../injection_container.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import '../../common/theme_shadcn.dart';
|
||||
import '../../../domain/usecases/equipment/search_equipment_usecase.dart';
|
||||
import '../../../data/models/equipment/equipment_dto.dart';
|
||||
|
||||
/// 장비 시리얼/바코드 검색 다이얼로그
|
||||
class EquipmentSearchDialog extends StatefulWidget {
|
||||
final Function(EquipmentDto equipment)? onEquipmentFound;
|
||||
|
||||
const EquipmentSearchDialog({super.key, this.onEquipmentFound});
|
||||
|
||||
@override
|
||||
State<EquipmentSearchDialog> createState() => _EquipmentSearchDialogState();
|
||||
}
|
||||
|
||||
class _EquipmentSearchDialogState extends State<EquipmentSearchDialog> {
|
||||
final _serialController = TextEditingController();
|
||||
final _barcodeController = TextEditingController();
|
||||
|
||||
late final GetEquipmentBySerialUseCase _serialUseCase;
|
||||
late final GetEquipmentByBarcodeUseCase _barcodeUseCase;
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
EquipmentDto? _foundEquipment;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serialUseCase = sl<GetEquipmentBySerialUseCase>();
|
||||
_barcodeUseCase = sl<GetEquipmentByBarcodeUseCase>();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_serialController.dispose();
|
||||
_barcodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _searchBySerial() async {
|
||||
final serial = _serialController.text.trim();
|
||||
if (serial.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = '시리얼 번호를 입력해주세요.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_foundEquipment = null;
|
||||
});
|
||||
|
||||
final result = await _serialUseCase(serial);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
setState(() {
|
||||
_errorMessage = failure.message;
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
(equipment) {
|
||||
setState(() {
|
||||
_foundEquipment = equipment;
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _searchByBarcode() async {
|
||||
final barcode = _barcodeController.text.trim();
|
||||
if (barcode.isEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = '바코드를 입력해주세요.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
_foundEquipment = null;
|
||||
});
|
||||
|
||||
final result = await _barcodeUseCase(barcode);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
setState(() {
|
||||
_errorMessage = failure.message;
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
(equipment) {
|
||||
setState(() {
|
||||
_foundEquipment = equipment;
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShadDialog(
|
||||
child: SizedBox(
|
||||
width: 500,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 헤더
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('장비 검색', style: ShadcnTheme.headingH3),
|
||||
ShadButton.ghost(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 시리얼 검색
|
||||
Text('시리얼 번호로 검색', style: ShadcnTheme.bodyLarge.copyWith(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ShadInput(
|
||||
controller: _serialController,
|
||||
placeholder: const Text('시리얼 번호 입력'),
|
||||
onSubmitted: (_) => _searchBySerial(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadButton(
|
||||
onPressed: _isLoading ? null : _searchBySerial,
|
||||
child: const Text('검색'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 바코드 검색
|
||||
Text('바코드로 검색', style: ShadcnTheme.bodyLarge.copyWith(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ShadInput(
|
||||
controller: _barcodeController,
|
||||
placeholder: const Text('바코드 입력'),
|
||||
onSubmitted: (_) => _searchByBarcode(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ShadButton(
|
||||
onPressed: _isLoading ? null : _searchByBarcode,
|
||||
child: const Text('검색'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton.outline(
|
||||
onPressed: () => _showQRScanDialog(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.qr_code_scanner, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
const Text('QR 스캔'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 로딩 표시
|
||||
if (_isLoading)
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
ShadProgress(value: null),
|
||||
const SizedBox(height: 8),
|
||||
Text('검색 중...', style: ShadcnTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 오류 메시지
|
||||
if (_errorMessage != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: Colors.red, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: ShadcnTheme.bodyMedium.copyWith(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 검색 결과
|
||||
if (_foundEquipment != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'장비를 찾았습니다!',
|
||||
style: ShadcnTheme.bodyLarge.copyWith(
|
||||
color: Colors.green,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildEquipmentInfo(_foundEquipment!),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
ShadButton.outline(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('닫기'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ShadButton(
|
||||
onPressed: () {
|
||||
if (widget.onEquipmentFound != null) {
|
||||
widget.onEquipmentFound!(_foundEquipment!);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('선택'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 장비 정보 표시
|
||||
Widget _buildEquipmentInfo(EquipmentDto equipment) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('시리얼 번호', equipment.serialNumber),
|
||||
if (equipment.barcode != null)
|
||||
_buildInfoRow('바코드', equipment.barcode!),
|
||||
if (equipment.modelName != null)
|
||||
_buildInfoRow('모델명', equipment.modelName!),
|
||||
if (equipment.companyName != null)
|
||||
_buildInfoRow('소속 회사', equipment.companyName!),
|
||||
_buildInfoRow('활성 상태', equipment.isActive ? '활성' : '비활성'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
color: ShadcnTheme.foregroundMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: ShadcnTheme.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/// QR 스캔 다이얼로그 (임시 구현)
|
||||
void _showQRScanDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ShadDialog(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.qr_code_scanner, size: 64, color: ShadcnTheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'QR 스캔 기능',
|
||||
style: ShadcnTheme.headingH3,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'QR 스캔 기능은 추후 구현 예정입니다.',
|
||||
style: ShadcnTheme.bodyMedium.copyWith(
|
||||
color: ShadcnTheme.foregroundMuted,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ShadButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('확인'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 장비 검색 다이얼로그 표시 유틸리티
|
||||
Future<EquipmentDto?> showEquipmentSearchDialog(
|
||||
BuildContext context, {
|
||||
Function(EquipmentDto equipment)? onEquipmentFound,
|
||||
}) async {
|
||||
return await showDialog<EquipmentDto>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => EquipmentSearchDialog(
|
||||
onEquipmentFound: onEquipmentFound,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:superport/data/models/model_dto.dart';
|
||||
import 'package:superport/data/models/model/model_dto.dart';
|
||||
import 'package:superport/data/models/vendor_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';
|
||||
import 'package:superport/screens/common/widgets/standard_dropdown.dart';
|
||||
|
||||
/// Equipment 등록/수정 폼에서 사용할 Vendor→Model cascade 선택 위젯
|
||||
class EquipmentVendorModelSelector extends StatefulWidget {
|
||||
@@ -141,6 +141,7 @@ class _EquipmentVendorModelSelectorState extends State<EquipmentVendorModelSelec
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Vendor 선택 드롭다운
|
||||
_buildVendorDropdown(),
|
||||
@@ -153,96 +154,59 @@ class _EquipmentVendorModelSelectorState extends State<EquipmentVendorModelSelec
|
||||
}
|
||||
|
||||
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,
|
||||
orElse: () => VendorDto(
|
||||
id: value,
|
||||
name: '로딩중...',
|
||||
),
|
||||
);
|
||||
return Text(vendor.name);
|
||||
},
|
||||
onChanged: widget.isReadOnly ? null : _onVendorChanged,
|
||||
initialValue: _selectedVendorId,
|
||||
enabled: !widget.isReadOnly,
|
||||
),
|
||||
],
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
child: StandardIntDropdown<VendorDto>(
|
||||
label: widget.isReadOnly ? '제조사 * 🔒' : '제조사 *',
|
||||
isRequired: true,
|
||||
items: vendors,
|
||||
isLoading: _isLoadingVendors,
|
||||
selectedValue: _selectedVendorId != null
|
||||
? vendors.where((v) => v.id == _selectedVendorId).firstOrNull
|
||||
: null,
|
||||
onChanged: (VendorDto? selectedVendor) {
|
||||
if (!widget.isReadOnly) {
|
||||
_onVendorChanged(selectedVendor?.id);
|
||||
}
|
||||
},
|
||||
itemBuilder: (VendorDto vendor) => Text(vendor.name),
|
||||
selectedItemBuilder: (VendorDto vendor) => Text(vendor.name),
|
||||
idExtractor: (VendorDto vendor) => vendor.id!,
|
||||
placeholder: '제조사를 선택하세요',
|
||||
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,
|
||||
orElse: () => ModelDto(
|
||||
id: value,
|
||||
name: '로딩중...',
|
||||
vendorsId: 0,
|
||||
),
|
||||
);
|
||||
return Text(model.name);
|
||||
},
|
||||
onChanged: isEnabled ? _onModelChanged : null,
|
||||
initialValue: _selectedModelId,
|
||||
enabled: isEnabled,
|
||||
),
|
||||
],
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
child: StandardIntDropdown<ModelDto>(
|
||||
label: widget.isReadOnly ? '모델 * 🔒' : '모델 *',
|
||||
isRequired: true,
|
||||
items: _filteredModels,
|
||||
isLoading: _isLoadingModels,
|
||||
selectedValue: _selectedModelId != null
|
||||
? _filteredModels.where((m) => m.id == _selectedModelId).firstOrNull
|
||||
: null,
|
||||
onChanged: (ModelDto? selectedModel) {
|
||||
if (isEnabled) {
|
||||
_onModelChanged(selectedModel?.id);
|
||||
}
|
||||
},
|
||||
itemBuilder: (ModelDto model) => Text(model.name),
|
||||
selectedItemBuilder: (ModelDto model) => Text(model.name),
|
||||
idExtractor: (ModelDto model) => model.id ?? 0,
|
||||
placeholder: _selectedVendorId == null
|
||||
? '먼저 제조사를 선택하세요'
|
||||
: '모델을 선택하세요',
|
||||
enabled: isEnabled,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user