프로젝트 최초 커밋
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
|
||||
/// 장비 입고 폼 컨트롤러
|
||||
///
|
||||
/// 폼의 전체 상태, 유효성, 저장, 데이터 로딩 등 비즈니스 로직을 담당한다.
|
||||
class EquipmentInFormController {
|
||||
final MockDataService dataService;
|
||||
final int? equipmentInId;
|
||||
|
||||
// 폼 키
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
// 입력 상태 변수
|
||||
String manufacturer = '';
|
||||
String name = '';
|
||||
String category = '';
|
||||
String subCategory = '';
|
||||
String subSubCategory = '';
|
||||
String serialNumber = '';
|
||||
String barcode = '';
|
||||
int quantity = 1;
|
||||
DateTime inDate = DateTime.now();
|
||||
String equipmentType = EquipmentType.new_;
|
||||
bool hasSerialNumber = true;
|
||||
|
||||
// 워런티 관련 상태
|
||||
String? warrantyLicense;
|
||||
String? warrantyCode; // 워런티 코드(텍스트 입력)
|
||||
DateTime warrantyStartDate = DateTime.now();
|
||||
DateTime warrantyEndDate = DateTime.now().add(const Duration(days: 365));
|
||||
List<String> warrantyLicenses = [];
|
||||
|
||||
// 자동완성 데이터
|
||||
List<String> manufacturers = [];
|
||||
List<String> equipmentNames = [];
|
||||
// 카테고리 자동완성 데이터
|
||||
List<String> categories = [];
|
||||
List<String> subCategories = [];
|
||||
List<String> subSubCategories = [];
|
||||
|
||||
// 편집 모드 여부
|
||||
bool isEditMode = false;
|
||||
|
||||
// 입고지, 파트너사 관련 상태
|
||||
String? warehouseLocation;
|
||||
String? partnerCompany;
|
||||
List<String> warehouseLocations = [];
|
||||
List<String> partnerCompanies = [];
|
||||
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
EquipmentInFormController({required this.dataService, this.equipmentInId}) {
|
||||
isEditMode = equipmentInId != null;
|
||||
_loadManufacturers();
|
||||
_loadEquipmentNames();
|
||||
_loadCategories();
|
||||
_loadSubCategories();
|
||||
_loadSubSubCategories();
|
||||
_loadWarehouseLocations();
|
||||
_loadPartnerCompanies();
|
||||
_loadWarrantyLicenses();
|
||||
if (isEditMode) {
|
||||
_loadEquipmentIn();
|
||||
}
|
||||
}
|
||||
|
||||
// 제조사 목록 로드
|
||||
void _loadManufacturers() {
|
||||
manufacturers = dataService.getAllManufacturers();
|
||||
}
|
||||
|
||||
// 장비명 목록 로드
|
||||
void _loadEquipmentNames() {
|
||||
equipmentNames = dataService.getAllEquipmentNames();
|
||||
}
|
||||
|
||||
// 카테고리 목록 로드
|
||||
void _loadCategories() {
|
||||
categories = dataService.getAllCategories();
|
||||
}
|
||||
|
||||
// 서브카테고리 목록 로드
|
||||
void _loadSubCategories() {
|
||||
subCategories = dataService.getAllSubCategories();
|
||||
}
|
||||
|
||||
// 서브서브카테고리 목록 로드
|
||||
void _loadSubSubCategories() {
|
||||
subSubCategories = dataService.getAllSubSubCategories();
|
||||
}
|
||||
|
||||
// 입고지 목록 로드
|
||||
void _loadWarehouseLocations() {
|
||||
warehouseLocations =
|
||||
dataService.getAllWarehouseLocations().map((e) => e.name).toList();
|
||||
}
|
||||
|
||||
// 파트너사 목록 로드
|
||||
void _loadPartnerCompanies() {
|
||||
partnerCompanies =
|
||||
dataService
|
||||
.getAllCompanies()
|
||||
.where((c) => c.companyTypes.contains(CompanyType.partner))
|
||||
.map((c) => c.name)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 워런티 라이센스 목록 로드
|
||||
void _loadWarrantyLicenses() {
|
||||
// 실제로는 API나 서비스에서 불러와야 하지만, 파트너사와 동일한 데이터 사용
|
||||
warrantyLicenses = List.from(partnerCompanies);
|
||||
}
|
||||
|
||||
// 기존 데이터 로드(수정 모드)
|
||||
void _loadEquipmentIn() {
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
manufacturer = equipmentIn.equipment.manufacturer;
|
||||
name = equipmentIn.equipment.name;
|
||||
category = equipmentIn.equipment.category;
|
||||
subCategory = equipmentIn.equipment.subCategory;
|
||||
subSubCategory = equipmentIn.equipment.subSubCategory;
|
||||
serialNumber = equipmentIn.equipment.serialNumber ?? '';
|
||||
barcode = equipmentIn.equipment.barcode ?? '';
|
||||
quantity = equipmentIn.equipment.quantity;
|
||||
inDate = equipmentIn.inDate;
|
||||
hasSerialNumber = serialNumber.isNotEmpty;
|
||||
equipmentType = equipmentIn.type;
|
||||
warehouseLocation = equipmentIn.warehouseLocation;
|
||||
partnerCompany = equipmentIn.partnerCompany;
|
||||
remarkController.text = equipmentIn.remark ?? '';
|
||||
|
||||
// 워런티 정보 로드 (실제 구현에서는 기존 값을 불러옵니다)
|
||||
warrantyLicense = equipmentIn.partnerCompany; // 기본값으로 파트너사 이름 사용
|
||||
warrantyStartDate = equipmentIn.inDate;
|
||||
warrantyEndDate = equipmentIn.inDate.add(const Duration(days: 365));
|
||||
// 워런티 코드도 불러오도록(실제 구현시)
|
||||
warrantyCode = null; // TODO: 실제 데이터에서 불러올 경우 수정
|
||||
}
|
||||
}
|
||||
|
||||
// 워런티 기간 계산
|
||||
String getWarrantyPeriodSummary() {
|
||||
final difference = warrantyEndDate.difference(warrantyStartDate);
|
||||
final days = difference.inDays;
|
||||
|
||||
if (days <= 0) {
|
||||
return '유효하지 않은 기간';
|
||||
}
|
||||
|
||||
final years = days ~/ 365;
|
||||
final remainingDays = days % 365;
|
||||
|
||||
String summary = '';
|
||||
if (years > 0) {
|
||||
summary += '$years년 ';
|
||||
}
|
||||
if (remainingDays > 0) {
|
||||
summary += '$remainingDays일';
|
||||
}
|
||||
|
||||
return summary.trim();
|
||||
}
|
||||
|
||||
// 저장 처리
|
||||
bool save() {
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return false;
|
||||
}
|
||||
formKey.currentState!.save();
|
||||
|
||||
// 입력값이 리스트에 없으면 추가
|
||||
if (partnerCompany != null &&
|
||||
partnerCompany!.isNotEmpty &&
|
||||
!partnerCompanies.contains(partnerCompany)) {
|
||||
partnerCompanies.add(partnerCompany!);
|
||||
}
|
||||
if (warehouseLocation != null &&
|
||||
warehouseLocation!.isNotEmpty &&
|
||||
!warehouseLocations.contains(warehouseLocation)) {
|
||||
warehouseLocations.add(warehouseLocation!);
|
||||
}
|
||||
if (manufacturer.isNotEmpty && !manufacturers.contains(manufacturer)) {
|
||||
manufacturers.add(manufacturer);
|
||||
}
|
||||
if (name.isNotEmpty && !equipmentNames.contains(name)) {
|
||||
equipmentNames.add(name);
|
||||
}
|
||||
if (category.isNotEmpty && !categories.contains(category)) {
|
||||
categories.add(category);
|
||||
}
|
||||
if (subCategory.isNotEmpty && !subCategories.contains(subCategory)) {
|
||||
subCategories.add(subCategory);
|
||||
}
|
||||
if (subSubCategory.isNotEmpty &&
|
||||
!subSubCategories.contains(subSubCategory)) {
|
||||
subSubCategories.add(subSubCategory);
|
||||
}
|
||||
if (warrantyLicense != null &&
|
||||
warrantyLicense!.isNotEmpty &&
|
||||
!warrantyLicenses.contains(warrantyLicense)) {
|
||||
warrantyLicenses.add(warrantyLicense!);
|
||||
}
|
||||
|
||||
final equipment = Equipment(
|
||||
manufacturer: manufacturer,
|
||||
name: name,
|
||||
category: category,
|
||||
subCategory: subCategory,
|
||||
subSubCategory: subSubCategory,
|
||||
serialNumber: hasSerialNumber ? serialNumber : null,
|
||||
barcode: barcode.isNotEmpty ? barcode : null,
|
||||
quantity: quantity,
|
||||
remark: remarkController.text.trim(),
|
||||
warrantyLicense: warrantyLicense,
|
||||
warrantyStartDate: warrantyStartDate,
|
||||
warrantyEndDate: warrantyEndDate,
|
||||
// 워런티 코드 저장 필요시 여기에 추가
|
||||
);
|
||||
if (isEditMode) {
|
||||
final equipmentIn = dataService.getEquipmentInById(equipmentInId!);
|
||||
if (equipmentIn != null) {
|
||||
final updatedEquipmentIn = EquipmentIn(
|
||||
id: equipmentIn.id,
|
||||
equipment: equipment,
|
||||
inDate: inDate,
|
||||
status: equipmentIn.status,
|
||||
type: equipmentType,
|
||||
warehouseLocation: warehouseLocation,
|
||||
partnerCompany: partnerCompany,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.updateEquipmentIn(updatedEquipmentIn);
|
||||
}
|
||||
} else {
|
||||
final newEquipmentIn = EquipmentIn(
|
||||
equipment: equipment,
|
||||
inDate: inDate,
|
||||
type: equipmentType,
|
||||
warehouseLocation: warehouseLocation,
|
||||
partnerCompany: partnerCompany,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.addEquipmentIn(newEquipmentIn);
|
||||
}
|
||||
|
||||
// 저장 후 리스트 재로딩 (중복 방지 및 최신화)
|
||||
_loadManufacturers();
|
||||
_loadEquipmentNames();
|
||||
_loadCategories();
|
||||
_loadSubCategories();
|
||||
_loadSubSubCategories();
|
||||
_loadWarehouseLocations();
|
||||
_loadPartnerCompanies();
|
||||
_loadWarrantyLicenses();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
remarkController.dispose();
|
||||
}
|
||||
}
|
||||
170
lib/screens/equipment/controllers/equipment_list_controller.dart
Normal file
170
lib/screens/equipment/controllers/equipment_list_controller.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
|
||||
// companyTypeToString 함수 import
|
||||
import 'package:superport/utils/constants.dart'
|
||||
show companyTypeToString, CompanyType;
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
|
||||
// 장비 목록 화면의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class EquipmentListController {
|
||||
final MockDataService dataService;
|
||||
List<UnifiedEquipment> equipments = [];
|
||||
String? selectedStatusFilter;
|
||||
final Set<String> selectedEquipmentIds = {}; // 'id:status' 형식
|
||||
|
||||
EquipmentListController({required this.dataService});
|
||||
|
||||
// 데이터 로드 및 상태 필터 적용
|
||||
void loadData() {
|
||||
equipments = dataService.getAllEquipments();
|
||||
if (selectedStatusFilter != null) {
|
||||
equipments =
|
||||
equipments.where((e) => e.status == selectedStatusFilter).toList();
|
||||
}
|
||||
selectedEquipmentIds.clear();
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
void changeStatusFilter(String? status) {
|
||||
selectedStatusFilter = status;
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 장비 선택/해제 (모든 상태 지원)
|
||||
void selectEquipment(int? id, String status, bool? isSelected) {
|
||||
if (id == null || isSelected == null) return;
|
||||
final key = '$id:$status';
|
||||
if (isSelected) {
|
||||
selectedEquipmentIds.add(key);
|
||||
} else {
|
||||
selectedEquipmentIds.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 선택된 입고 장비 수 반환
|
||||
int getSelectedInStockCount() {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == EquipmentStatus.in_) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 전체 장비 수 반환
|
||||
int getSelectedEquipmentCount() {
|
||||
return selectedEquipmentIds.length;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비 수 반환
|
||||
int getSelectedEquipmentCountByStatus(String status) {
|
||||
int count = 0;
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipments() {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == parts[1],
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 특정 상태의 장비들의 UnifiedEquipment 객체 목록 반환
|
||||
List<UnifiedEquipment> getSelectedEquipmentsByStatus(String status) {
|
||||
List<UnifiedEquipment> selected = [];
|
||||
for (final idStatusPair in selectedEquipmentIds) {
|
||||
final parts = idStatusPair.split(':');
|
||||
if (parts.length == 2 && parts[1] == status) {
|
||||
final id = int.tryParse(parts[0]);
|
||||
if (id != null) {
|
||||
final equipment = equipments.firstWhere(
|
||||
(e) => e.id == id && e.status == status,
|
||||
orElse: () => null as UnifiedEquipment,
|
||||
);
|
||||
if (equipment != null) {
|
||||
selected.add(equipment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 Map 형태로 반환 (출고/대여/폐기 폼에서 사용)
|
||||
List<Map<String, dynamic>> getSelectedEquipmentsSummary() {
|
||||
List<Map<String, dynamic>> summaryList = [];
|
||||
List<UnifiedEquipment> selectedEquipmentsInStock =
|
||||
getSelectedEquipmentsByStatus(EquipmentStatus.in_);
|
||||
|
||||
for (final equipment in selectedEquipmentsInStock) {
|
||||
summaryList.add({
|
||||
'equipment': equipment.equipment,
|
||||
'equipmentInId': equipment.id,
|
||||
'status': equipment.status,
|
||||
});
|
||||
}
|
||||
|
||||
return summaryList;
|
||||
}
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등) 반환
|
||||
String getOutEquipmentInfo(int equipmentId, String infoType) {
|
||||
final equipmentOut = dataService.getEquipmentOutById(equipmentId);
|
||||
if (equipmentOut != null) {
|
||||
switch (infoType) {
|
||||
case 'company':
|
||||
final company = equipmentOut.company ?? '-';
|
||||
if (company != '-') {
|
||||
final companyObj = dataService.getAllCompanies().firstWhere(
|
||||
(c) => c.name == company,
|
||||
orElse:
|
||||
() => Company(
|
||||
name: company,
|
||||
address: Address(),
|
||||
companyTypes: [CompanyType.customer], // 기본값 고객사
|
||||
),
|
||||
);
|
||||
// 여러 유형 중 첫 번째만 표시 (대표 유형)
|
||||
final typeText =
|
||||
companyObj.companyTypes.isNotEmpty
|
||||
? companyTypeToString(companyObj.companyTypes.first)
|
||||
: '-';
|
||||
return '$company (${typeText})';
|
||||
}
|
||||
return company;
|
||||
case 'manager':
|
||||
return equipmentOut.manager ?? '-';
|
||||
case 'license':
|
||||
return equipmentOut.license ?? '-';
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
|
||||
// 장비 출고 폼의 상태 및 비즈니스 로직을 담당하는 컨트롤러
|
||||
class EquipmentOutFormController {
|
||||
final MockDataService dataService;
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final TextEditingController remarkController = TextEditingController();
|
||||
|
||||
// 상태 변수
|
||||
bool isEditMode = false;
|
||||
String manufacturer = '';
|
||||
String name = '';
|
||||
String category = '';
|
||||
String subCategory = '';
|
||||
String subSubCategory = '';
|
||||
String serialNumber = '';
|
||||
String barcode = '';
|
||||
int quantity = 1;
|
||||
DateTime outDate = DateTime.now();
|
||||
bool hasSerialNumber = false;
|
||||
DateTime? inDate;
|
||||
String returnType = '재입고';
|
||||
DateTime returnDate = DateTime.now();
|
||||
bool hasManagers = false;
|
||||
|
||||
// 출고 유형(출고/대여/폐기) 상태 변수 추가
|
||||
String outType = '출고'; // 기본값은 '출고'
|
||||
|
||||
// 기존 필드 - 호환성을 위해 유지
|
||||
String? _selectedCompany;
|
||||
String? get selectedCompany =>
|
||||
selectedCompanies.isNotEmpty ? selectedCompanies[0] : null;
|
||||
set selectedCompany(String? value) {
|
||||
if (selectedCompanies.isEmpty) {
|
||||
selectedCompanies.add(value);
|
||||
} else {
|
||||
selectedCompanies[0] = value;
|
||||
}
|
||||
_selectedCompany = value;
|
||||
}
|
||||
|
||||
String? _selectedManager;
|
||||
String? get selectedManager =>
|
||||
selectedManagersPerCompany.isNotEmpty
|
||||
? selectedManagersPerCompany[0]
|
||||
: null;
|
||||
set selectedManager(String? value) {
|
||||
if (selectedManagersPerCompany.isEmpty) {
|
||||
selectedManagersPerCompany.add(value);
|
||||
} else {
|
||||
selectedManagersPerCompany[0] = value;
|
||||
}
|
||||
_selectedManager = value;
|
||||
}
|
||||
|
||||
String? selectedLicense;
|
||||
List<String> companies = [];
|
||||
// 회사 및 지점 관련 데이터
|
||||
List<CompanyBranchInfo> companiesWithBranches = [];
|
||||
List<String> managers = [];
|
||||
List<String> filteredManagers = [];
|
||||
List<String> licenses = [];
|
||||
|
||||
// 출고 회사 목록 관리
|
||||
List<String?> selectedCompanies = [null]; // 첫 번째 드롭다운을 위한 초기값
|
||||
List<List<String>> availableCompaniesPerDropdown =
|
||||
[]; // 각 드롭다운마다 사용 가능한 회사 목록
|
||||
List<String?> selectedManagersPerCompany = [null]; // 각 드롭다운 회사별 선택된 담당자
|
||||
List<List<String>> filteredManagersPerCompany = []; // 각 드롭다운 회사별 필터링된 담당자 목록
|
||||
List<bool> hasManagersPerCompany = [false]; // 각 회사별 담당자 유무
|
||||
|
||||
// 입력 데이터
|
||||
Equipment? selectedEquipment;
|
||||
int? selectedEquipmentInId;
|
||||
int? equipmentOutId;
|
||||
List<Map<String, dynamic>>? _selectedEquipments;
|
||||
|
||||
EquipmentOutFormController({required this.dataService});
|
||||
|
||||
// 선택된 장비 정보 설정 (디버그용)
|
||||
set selectedEquipments(List<Map<String, dynamic>>? equipments) {
|
||||
debugPrint('설정된 장비 목록: ${equipments?.length ?? 0}개');
|
||||
if (equipments != null) {
|
||||
for (var i = 0; i < equipments.length; i++) {
|
||||
final equipment = equipments[i]['equipment'] as Equipment;
|
||||
debugPrint('장비 $i: ${equipment.manufacturer} ${equipment.name}');
|
||||
}
|
||||
}
|
||||
_selectedEquipments = equipments;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>>? get selectedEquipments => _selectedEquipments;
|
||||
|
||||
// 드롭다운 데이터 로드
|
||||
void loadDropdownData() {
|
||||
final allCompanies = dataService.getAllCompanies();
|
||||
|
||||
// 회사와 지점 통합 목록 생성
|
||||
companiesWithBranches = [];
|
||||
companies = [];
|
||||
|
||||
for (var company in allCompanies) {
|
||||
// 회사 자체 정보 추가
|
||||
final companyType =
|
||||
company.companyTypes.isNotEmpty
|
||||
? companyTypeToString(company.companyTypes.first)
|
||||
: '-';
|
||||
final companyInfo = CompanyBranchInfo(
|
||||
id: company.id,
|
||||
name: "${company.name} (${companyType})",
|
||||
originalName: company.name,
|
||||
isMainCompany: true,
|
||||
companyId: company.id,
|
||||
branchId: null,
|
||||
);
|
||||
companiesWithBranches.add(companyInfo);
|
||||
companies.add(companyInfo.name);
|
||||
|
||||
// 지점 정보 추가
|
||||
if (company.branches != null && company.branches!.isNotEmpty) {
|
||||
for (var branch in company.branches!) {
|
||||
final branchInfo = CompanyBranchInfo(
|
||||
id: branch.id,
|
||||
name: "${company.name} ${branch.name}",
|
||||
displayName: branch.name,
|
||||
originalName: branch.name,
|
||||
isMainCompany: false,
|
||||
companyId: company.id,
|
||||
branchId: branch.id,
|
||||
parentCompanyName: company.name,
|
||||
);
|
||||
companiesWithBranches.add(branchInfo);
|
||||
companies.add(branchInfo.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 나머지 데이터 로드
|
||||
final allUsers = dataService.getAllUsers();
|
||||
managers = allUsers.map((user) => user.name).toList();
|
||||
filteredManagers = managers;
|
||||
final allLicenses = dataService.getAllLicenses();
|
||||
licenses = allLicenses.map((license) => license.name).toList();
|
||||
if (companies.isEmpty) companies.add('기타');
|
||||
if (managers.isEmpty) managers.add('기타');
|
||||
if (licenses.isEmpty) licenses.add('기타');
|
||||
updateManagersState();
|
||||
|
||||
// 출고 회사 드롭다운 초기화
|
||||
availableCompaniesPerDropdown = [List.from(companies)];
|
||||
filteredManagersPerCompany = [List.from(managers)];
|
||||
hasManagersPerCompany = [hasManagers];
|
||||
|
||||
// 디버그 정보 출력
|
||||
debugPrint('드롭다운 데이터 로드 완료');
|
||||
debugPrint('장비 목록: ${_selectedEquipments?.length ?? 0}개');
|
||||
debugPrint('회사 및 지점 목록: ${companiesWithBranches.length}개');
|
||||
|
||||
// 수정 모드인 경우 기존 선택값 동기화
|
||||
if (isEditMode && equipmentOutId != null) {
|
||||
final equipmentOut = dataService.getEquipmentOutById(equipmentOutId!);
|
||||
if (equipmentOut != null && equipmentOut.company != null) {
|
||||
String companyName = '';
|
||||
|
||||
// 회사 이름 찾기
|
||||
for (String company in companies) {
|
||||
if (company.startsWith(equipmentOut.company!)) {
|
||||
companyName = company;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (companyName.isNotEmpty) {
|
||||
selectedCompanies[0] = companyName;
|
||||
filterManagersByCompanyAtIndex(companyName, 0);
|
||||
|
||||
// 기존 담당자 설정
|
||||
if (equipmentOut.manager != null) {
|
||||
selectedManagersPerCompany[0] = equipmentOut.manager;
|
||||
}
|
||||
}
|
||||
|
||||
// 라이센스 설정
|
||||
if (equipmentOut.license != null) {
|
||||
selectedLicense = equipmentOut.license;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 회사에 따라 담당자 목록 필터링
|
||||
void filterManagersByCompany(String? companyName) {
|
||||
if (companyName == null || companyName.isEmpty) {
|
||||
filteredManagers = managers;
|
||||
} else {
|
||||
// 회사 또는 지점 이름에서 CompanyBranchInfo 찾기
|
||||
CompanyBranchInfo? companyInfo = _findCompanyInfoByDisplayName(
|
||||
companyName,
|
||||
);
|
||||
|
||||
if (companyInfo != null && companyInfo.companyId != null) {
|
||||
int companyId = companyInfo.companyId!;
|
||||
final companyUsers =
|
||||
dataService
|
||||
.getAllUsers()
|
||||
.where((user) => user.companyId == companyId)
|
||||
.toList();
|
||||
|
||||
if (companyUsers.isNotEmpty) {
|
||||
filteredManagers = companyUsers.map((user) => user.name).toList();
|
||||
} else {
|
||||
filteredManagers = ['없음'];
|
||||
}
|
||||
} else {
|
||||
filteredManagers = ['없음'];
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedManager != null &&
|
||||
!filteredManagers.contains(selectedManager)) {
|
||||
selectedManager =
|
||||
filteredManagers.isNotEmpty ? filteredManagers[0] : null;
|
||||
}
|
||||
updateManagersState();
|
||||
|
||||
// 첫 번째 회사에 대한 담당자 목록과 동기화
|
||||
if (filteredManagersPerCompany.isNotEmpty) {
|
||||
filteredManagersPerCompany[0] = List.from(filteredManagers);
|
||||
hasManagersPerCompany[0] = hasManagers;
|
||||
if (selectedManagersPerCompany.isNotEmpty) {
|
||||
selectedManagersPerCompany[0] = selectedManager;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 인덱스의 회사에 따라 담당자 목록 필터링
|
||||
void filterManagersByCompanyAtIndex(String? companyName, int index) {
|
||||
if (companyName == null || companyName.isEmpty) {
|
||||
filteredManagersPerCompany[index] = managers;
|
||||
} else {
|
||||
// 회사 또는 지점 이름에서 CompanyBranchInfo 찾기
|
||||
CompanyBranchInfo? companyInfo = _findCompanyInfoByDisplayName(
|
||||
companyName,
|
||||
);
|
||||
|
||||
if (companyInfo != null && companyInfo.companyId != null) {
|
||||
int companyId = companyInfo.companyId!;
|
||||
final companyUsers =
|
||||
dataService
|
||||
.getAllUsers()
|
||||
.where((user) => user.companyId == companyId)
|
||||
.toList();
|
||||
|
||||
if (companyUsers.isNotEmpty) {
|
||||
filteredManagersPerCompany[index] =
|
||||
companyUsers.map((user) => user.name).toList();
|
||||
} else {
|
||||
filteredManagersPerCompany[index] = ['없음'];
|
||||
}
|
||||
} else {
|
||||
filteredManagersPerCompany[index] = ['없음'];
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedManagersPerCompany[index] != null &&
|
||||
!filteredManagersPerCompany[index].contains(
|
||||
selectedManagersPerCompany[index],
|
||||
)) {
|
||||
selectedManagersPerCompany[index] =
|
||||
filteredManagersPerCompany[index].isNotEmpty
|
||||
? filteredManagersPerCompany[index][0]
|
||||
: null;
|
||||
}
|
||||
updateManagersStateAtIndex(index);
|
||||
|
||||
// 첫 번째 회사인 경우 기존 필드와 동기화
|
||||
if (index == 0) {
|
||||
filteredManagers = List.from(filteredManagersPerCompany[0]);
|
||||
hasManagers = hasManagersPerCompany[0];
|
||||
_selectedManager = selectedManagersPerCompany[0];
|
||||
}
|
||||
}
|
||||
|
||||
// 담당자 있는지 상태 업데이트
|
||||
void updateManagersState() {
|
||||
hasManagers =
|
||||
filteredManagers.isNotEmpty &&
|
||||
!(filteredManagers.length == 1 && filteredManagers[0] == '없음');
|
||||
}
|
||||
|
||||
// 특정 인덱스의 담당자 상태 업데이트
|
||||
void updateManagersStateAtIndex(int index) {
|
||||
hasManagersPerCompany[index] =
|
||||
filteredManagersPerCompany[index].isNotEmpty &&
|
||||
!(filteredManagersPerCompany[index].length == 1 &&
|
||||
filteredManagersPerCompany[index][0] == '없음');
|
||||
}
|
||||
|
||||
// 출고 회사 추가
|
||||
void addCompany() {
|
||||
// 이미 선택된 회사 제외한 리스트 생성
|
||||
List<String> availableCompanies = List.from(companies);
|
||||
for (String? company in selectedCompanies) {
|
||||
if (company != null) {
|
||||
availableCompanies.remove(company);
|
||||
}
|
||||
}
|
||||
|
||||
// 새 드롭다운 추가
|
||||
selectedCompanies.add(null);
|
||||
availableCompaniesPerDropdown.add(availableCompanies);
|
||||
selectedManagersPerCompany.add(null);
|
||||
filteredManagersPerCompany.add(List.from(managers));
|
||||
hasManagersPerCompany.add(false);
|
||||
}
|
||||
|
||||
// 가능한 회사 목록 업데이트
|
||||
void updateAvailableCompanies() {
|
||||
// 각 드롭다운에 대해 사용 가능한 회사 목록 업데이트
|
||||
for (int i = 0; i < selectedCompanies.length; i++) {
|
||||
List<String> availableCompanies = List.from(companies);
|
||||
|
||||
// 이미 선택된 회사 제외
|
||||
for (int j = 0; j < selectedCompanies.length; j++) {
|
||||
if (i != j && selectedCompanies[j] != null) {
|
||||
availableCompanies.remove(selectedCompanies[j]);
|
||||
}
|
||||
}
|
||||
|
||||
availableCompaniesPerDropdown[i] = availableCompanies;
|
||||
}
|
||||
}
|
||||
|
||||
// 선택 장비로 초기화
|
||||
void initializeWithSelectedEquipment(Equipment equipment) {
|
||||
manufacturer = equipment.manufacturer;
|
||||
name = equipment.name;
|
||||
category = equipment.category;
|
||||
subCategory = equipment.subCategory;
|
||||
subSubCategory = equipment.subSubCategory;
|
||||
serialNumber = equipment.serialNumber ?? '';
|
||||
barcode = equipment.barcode ?? '';
|
||||
quantity = equipment.quantity;
|
||||
hasSerialNumber = serialNumber.isNotEmpty;
|
||||
inDate = equipment.inDate;
|
||||
remarkController.text = equipment.remark ?? '';
|
||||
}
|
||||
|
||||
// 회사/지점 표시 이름을 통해 CompanyBranchInfo 객체 찾기
|
||||
CompanyBranchInfo? _findCompanyInfoByDisplayName(String displayName) {
|
||||
for (var info in companiesWithBranches) {
|
||||
if (info.name == displayName) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 출고 정보 저장 (UI에서 호출)
|
||||
void saveEquipmentOut(Function(String) onSuccess, Function(String) onError) {
|
||||
if (formKey.currentState?.validate() != true) {
|
||||
onError('폼 유효성 검사 실패');
|
||||
return;
|
||||
}
|
||||
formKey.currentState?.save();
|
||||
|
||||
// 선택된 회사가 없는지 확인
|
||||
bool hasAnySelectedCompany = selectedCompanies.any(
|
||||
(company) => company != null,
|
||||
);
|
||||
if (!hasAnySelectedCompany) {
|
||||
onError('최소 하나의 출고 회사를 선택해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 방식으로 첫 번째 회사 정보 처리
|
||||
String? companyName;
|
||||
if (selectedCompanies.isNotEmpty && selectedCompanies[0] != null) {
|
||||
CompanyBranchInfo? companyInfo = _findCompanyInfoByDisplayName(
|
||||
selectedCompanies[0]!,
|
||||
);
|
||||
if (companyInfo != null) {
|
||||
companyName =
|
||||
companyInfo.isMainCompany
|
||||
? companyInfo
|
||||
.originalName // 본사인 경우 회사 원래 이름
|
||||
: "${companyInfo.originalName} (${companyInfo.branchId})"; // 지점인 경우 지점 정보 포함
|
||||
} else {
|
||||
companyName = selectedCompanies[0]!.replaceAll(
|
||||
RegExp(r' \(.*\)\$'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onError('최소 하나의 출고 회사를 선택해주세요');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode && equipmentOutId != null) {
|
||||
final equipmentOut = dataService.getEquipmentOutById(equipmentOutId!);
|
||||
if (equipmentOut != null) {
|
||||
final updatedEquipmentOut = EquipmentOut(
|
||||
id: equipmentOut.id,
|
||||
equipment: equipmentOut.equipment,
|
||||
outDate: equipmentOut.outDate,
|
||||
status: returnType == '재입고' ? 'I' : 'R',
|
||||
company: companyName,
|
||||
manager: equipmentOut.manager,
|
||||
license: equipmentOut.license,
|
||||
returnDate: returnDate,
|
||||
returnType: returnType,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.updateEquipmentOut(updatedEquipmentOut);
|
||||
onSuccess('장비 출고 상태 변경 완료');
|
||||
} else {
|
||||
onError('출고 정보를 찾을 수 없습니다');
|
||||
}
|
||||
} else {
|
||||
if (selectedEquipments != null && selectedEquipments!.isNotEmpty) {
|
||||
// 여러 회사에 각각 출고 처리
|
||||
List<String> successCompanies = [];
|
||||
|
||||
// 선택된 모든 회사에 대해 출고 처리
|
||||
for (int i = 0; i < selectedCompanies.length; i++) {
|
||||
if (selectedCompanies[i] == null) continue;
|
||||
|
||||
CompanyBranchInfo? companyInfo = _findCompanyInfoByDisplayName(
|
||||
selectedCompanies[i]!,
|
||||
);
|
||||
String curCompanyName;
|
||||
|
||||
if (companyInfo != null) {
|
||||
curCompanyName =
|
||||
companyInfo.isMainCompany
|
||||
? companyInfo
|
||||
.originalName // 본사인 경우 회사 원래 이름
|
||||
: "${companyInfo.originalName} (${companyInfo.branchId})"; // 지점인 경우 지점 정보 포함
|
||||
} else {
|
||||
curCompanyName = selectedCompanies[i]!.replaceAll(
|
||||
RegExp(r' \(.*\)\$'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
String? curManager = selectedManagersPerCompany[i];
|
||||
|
||||
if (curManager == null || curManager == '없음') {
|
||||
// 담당자 없는 회사는 건너뛰기
|
||||
continue;
|
||||
}
|
||||
|
||||
// 해당 회사에 모든 장비 출고 처리
|
||||
for (final equipmentData in selectedEquipments!) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
final equipmentInId = equipmentData['equipmentInId'] as int;
|
||||
final newEquipmentOut = EquipmentOut(
|
||||
equipment: equipment,
|
||||
outDate: outDate,
|
||||
company: curCompanyName,
|
||||
manager: curManager,
|
||||
license: selectedLicense,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.changeEquipmentStatus(equipmentInId, newEquipmentOut);
|
||||
}
|
||||
|
||||
successCompanies.add(companyInfo?.name ?? curCompanyName);
|
||||
}
|
||||
|
||||
if (successCompanies.isEmpty) {
|
||||
onError('모든 회사에 담당자가 없어 출고 처리할 수 없습니다');
|
||||
} else {
|
||||
onSuccess('${successCompanies.join(", ")} 회사로 다중 장비 출고 처리 완료');
|
||||
}
|
||||
} else if (selectedEquipmentInId != null) {
|
||||
final equipment = Equipment(
|
||||
manufacturer: manufacturer,
|
||||
name: name,
|
||||
category: category,
|
||||
subCategory: subCategory,
|
||||
subSubCategory: subSubCategory,
|
||||
serialNumber: (hasSerialNumber) ? serialNumber : null,
|
||||
barcode: barcode.isNotEmpty ? barcode : null,
|
||||
quantity: quantity,
|
||||
inDate: inDate,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
|
||||
// 선택된 모든 회사에 대해 출고 처리
|
||||
List<String> successCompanies = [];
|
||||
|
||||
for (int i = 0; i < selectedCompanies.length; i++) {
|
||||
if (selectedCompanies[i] == null) continue;
|
||||
|
||||
CompanyBranchInfo? companyInfo = _findCompanyInfoByDisplayName(
|
||||
selectedCompanies[i]!,
|
||||
);
|
||||
String curCompanyName;
|
||||
|
||||
if (companyInfo != null) {
|
||||
curCompanyName =
|
||||
companyInfo.isMainCompany
|
||||
? companyInfo
|
||||
.originalName // 본사인 경우 회사 원래 이름
|
||||
: "${companyInfo.originalName} (${companyInfo.branchId})"; // 지점인 경우 지점 정보 포함
|
||||
} else {
|
||||
curCompanyName = selectedCompanies[i]!.replaceAll(
|
||||
RegExp(r' \(.*\)\$'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
String? curManager = selectedManagersPerCompany[i];
|
||||
|
||||
if (curManager == null || curManager == '없음') {
|
||||
// 담당자 없는 회사는 건너뛰기
|
||||
continue;
|
||||
}
|
||||
|
||||
final newEquipmentOut = EquipmentOut(
|
||||
equipment: equipment,
|
||||
outDate: outDate,
|
||||
company: curCompanyName,
|
||||
manager: curManager,
|
||||
license: selectedLicense,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.changeEquipmentStatus(
|
||||
selectedEquipmentInId!,
|
||||
newEquipmentOut,
|
||||
);
|
||||
|
||||
successCompanies.add(companyInfo?.name ?? curCompanyName);
|
||||
break; // 한 장비는 한 회사에만 출고
|
||||
}
|
||||
|
||||
if (successCompanies.isEmpty) {
|
||||
onError('모든 회사에 담당자가 없어 출고 처리할 수 없습니다');
|
||||
} else {
|
||||
onSuccess('${successCompanies.join(", ")} 회사로 장비 출고 처리 완료');
|
||||
}
|
||||
} else {
|
||||
final equipment = Equipment(
|
||||
manufacturer: manufacturer,
|
||||
name: name,
|
||||
category: category,
|
||||
subCategory: subCategory,
|
||||
subSubCategory: subSubCategory,
|
||||
serialNumber: null,
|
||||
barcode: null,
|
||||
quantity: 1,
|
||||
inDate: inDate,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
|
||||
// 선택된 모든 회사에 대해 출고 처리
|
||||
List<String> successCompanies = [];
|
||||
|
||||
for (int i = 0; i < selectedCompanies.length; i++) {
|
||||
if (selectedCompanies[i] == null) continue;
|
||||
|
||||
CompanyBranchInfo? companyInfo = _findCompanyInfoByDisplayName(
|
||||
selectedCompanies[i]!,
|
||||
);
|
||||
String curCompanyName;
|
||||
|
||||
if (companyInfo != null) {
|
||||
curCompanyName =
|
||||
companyInfo.isMainCompany
|
||||
? companyInfo
|
||||
.originalName // 본사인 경우 회사 원래 이름
|
||||
: "${companyInfo.originalName} (${companyInfo.branchId})"; // 지점인 경우 지점 정보 포함
|
||||
} else {
|
||||
curCompanyName = selectedCompanies[i]!.replaceAll(
|
||||
RegExp(r' \(.*\)\$'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
String? curManager = selectedManagersPerCompany[i];
|
||||
|
||||
if (curManager == null || curManager == '없음') {
|
||||
// 담당자 없는 회사는 건너뛰기
|
||||
continue;
|
||||
}
|
||||
|
||||
final newEquipmentOut = EquipmentOut(
|
||||
equipment: equipment,
|
||||
outDate: outDate,
|
||||
company: curCompanyName,
|
||||
manager: curManager,
|
||||
license: selectedLicense,
|
||||
remark: remarkController.text.trim(),
|
||||
);
|
||||
dataService.addEquipmentOut(newEquipmentOut);
|
||||
|
||||
successCompanies.add(companyInfo?.name ?? curCompanyName);
|
||||
}
|
||||
|
||||
if (successCompanies.isEmpty) {
|
||||
onError('모든 회사에 담당자가 없어 출고 처리할 수 없습니다');
|
||||
} else {
|
||||
onSuccess('${successCompanies.join(", ")} 회사로 새 출고 장비 추가 완료');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 날짜 포맷 유틸리티
|
||||
String formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
remarkController.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// 회사 및 지점 정보를 저장하는 클래스
|
||||
class CompanyBranchInfo {
|
||||
final int? id;
|
||||
final String name; // 표시용 이름 (회사명 + 지점명 또는 회사명 (유형))
|
||||
final String originalName; // 원래 이름 (회사 본사명 또는 지점명)
|
||||
final String? displayName; // UI에 표시할 이름 (주로 지점명)
|
||||
final bool isMainCompany; // 본사인지 지점인지 구분
|
||||
final int? companyId; // 회사 ID
|
||||
final int? branchId; // 지점 ID
|
||||
final String? parentCompanyName; // 부모 회사명 (지점인 경우)
|
||||
|
||||
CompanyBranchInfo({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.originalName,
|
||||
this.displayName,
|
||||
required this.isMainCompany,
|
||||
required this.companyId,
|
||||
required this.branchId,
|
||||
this.parentCompanyName,
|
||||
});
|
||||
}
|
||||
2267
lib/screens/equipment/equipment_in_form.dart
Normal file
2267
lib/screens/equipment/equipment_in_form.dart
Normal file
File diff suppressed because it is too large
Load Diff
696
lib/screens/equipment/equipment_list.dart
Normal file
696
lib/screens/equipment/equipment_list.dart
Normal file
@@ -0,0 +1,696 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_list_controller.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_table.dart';
|
||||
import 'package:superport/utils/equipment_display_helper.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/screens/common/main_layout.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/screens/common/widgets/pagination.dart';
|
||||
|
||||
// 장비 목록 화면 (UI만 담당, 상태/로직/헬퍼/위젯 분리)
|
||||
class EquipmentListScreen extends StatefulWidget {
|
||||
final String currentRoute;
|
||||
const EquipmentListScreen({super.key, this.currentRoute = Routes.equipment});
|
||||
|
||||
@override
|
||||
State<EquipmentListScreen> createState() => _EquipmentListScreenState();
|
||||
}
|
||||
|
||||
class _EquipmentListScreenState extends State<EquipmentListScreen> {
|
||||
late final EquipmentListController _controller;
|
||||
bool _showDetailedColumns = true;
|
||||
final ScrollController _horizontalScrollController = ScrollController();
|
||||
final ScrollController _verticalScrollController = ScrollController();
|
||||
int _currentPage = 1;
|
||||
final int _pageSize = 10;
|
||||
String _searchKeyword = '';
|
||||
String _appliedSearchKeyword = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentListController(dataService: MockDataService());
|
||||
_controller.loadData();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_adjustColumnsForScreenSize();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_setDefaultFilterByRoute();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EquipmentListScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentRoute != widget.currentRoute) {
|
||||
_setDefaultFilterByRoute();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_horizontalScrollController.dispose();
|
||||
_verticalScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 라우트에 따라 기본 필터 설정
|
||||
void _setDefaultFilterByRoute() {
|
||||
String? newFilter;
|
||||
if (widget.currentRoute == Routes.equipmentInList) {
|
||||
newFilter = EquipmentStatus.in_;
|
||||
} else if (widget.currentRoute == Routes.equipmentOutList) {
|
||||
newFilter = EquipmentStatus.out;
|
||||
} else if (widget.currentRoute == Routes.equipmentRentList) {
|
||||
newFilter = EquipmentStatus.rent;
|
||||
} else if (widget.currentRoute == Routes.equipment) {
|
||||
newFilter = null;
|
||||
}
|
||||
if ((newFilter != _controller.selectedStatusFilter) ||
|
||||
widget.currentRoute != Routes.equipment) {
|
||||
setState(() {
|
||||
_controller.selectedStatusFilter = newFilter;
|
||||
_controller.loadData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 화면 크기에 따라 컬럼 표시 조정
|
||||
void _adjustColumnsForScreenSize() {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
setState(() {
|
||||
_showDetailedColumns = width > 900;
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터 변경
|
||||
void _onStatusFilterChanged(String? status) {
|
||||
setState(() {
|
||||
_controller.changeStatusFilter(status);
|
||||
});
|
||||
}
|
||||
|
||||
// 장비 선택/해제
|
||||
void _onEquipmentSelected(int? id, String status, bool? isSelected) {
|
||||
setState(() {
|
||||
_controller.selectEquipment(id, status, isSelected);
|
||||
});
|
||||
}
|
||||
|
||||
// 출고 처리 버튼 핸들러
|
||||
void _handleOutEquipment() async {
|
||||
if (_controller.getSelectedInStockCount() == 0) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('출고할 장비를 선택해주세요.')));
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 가져와서 출고 폼으로 전달
|
||||
final selectedEquipmentsSummary =
|
||||
_controller.getSelectedEquipmentsSummary();
|
||||
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.equipmentOutAdd,
|
||||
arguments: {'selectedEquipments': selectedEquipmentsSummary},
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대여 처리 버튼 핸들러
|
||||
void _handleRentEquipment() async {
|
||||
if (_controller.getSelectedInStockCount() == 0) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('대여할 장비를 선택해주세요.')));
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 가져와서 대여 폼으로 전달
|
||||
final selectedEquipmentsSummary =
|
||||
_controller.getSelectedEquipmentsSummary();
|
||||
|
||||
// 현재는 대여 기능이 준비되지 않았으므로 간단히 스낵바 표시
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${selectedEquipmentsSummary.length}개 장비 대여 기능은 준비 중입니다.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 폐기 처리 버튼 핸들러
|
||||
void _handleDisposeEquipment() {
|
||||
if (_controller.getSelectedInStockCount() == 0) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('폐기할 장비를 선택해주세요.')));
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 장비들의 요약 정보를 가져옴
|
||||
final selectedEquipmentsSummary =
|
||||
_controller.getSelectedEquipmentsSummary();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('폐기 확인'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'선택한 ${selectedEquipmentsSummary.length}개 장비를 폐기하시겠습니까?',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'폐기할 장비 목록:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...selectedEquipmentsSummary.map((equipmentData) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
'${equipment.manufacturer} ${equipment.name} (${equipment.quantity}개)',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// 여기에 폐기 로직 추가 예정
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('폐기 기능은 준비 중입니다.')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('폐기'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 카테고리 축약 표기 함수 (예: 컴... > 태... > 안드로...)
|
||||
String _shortenCategory(String category) {
|
||||
if (category.length <= 2) return category;
|
||||
return category.substring(0, 2) + '...';
|
||||
}
|
||||
|
||||
// 카테고리 툴팁 위젯 (UI만 담당, 축약 표기 적용)
|
||||
Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) {
|
||||
final fullCategory = EquipmentDisplayHelper.formatCategory(
|
||||
equipment.equipment.category,
|
||||
equipment.equipment.subCategory,
|
||||
equipment.equipment.subSubCategory,
|
||||
);
|
||||
// 축약 표기 적용
|
||||
final shortCategory = [
|
||||
_shortenCategory(equipment.equipment.category),
|
||||
_shortenCategory(equipment.equipment.subCategory),
|
||||
_shortenCategory(equipment.equipment.subSubCategory),
|
||||
].join(' > ');
|
||||
return Tooltip(message: fullCategory, child: Text(shortCategory));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final maxContentWidth = screenWidth > 1200 ? 1200.0 : screenWidth - 32;
|
||||
String screenTitle = '장비 목록';
|
||||
if (widget.currentRoute == Routes.equipmentInList) {
|
||||
screenTitle = '입고된 장비';
|
||||
} else if (widget.currentRoute == Routes.equipmentOutList) {
|
||||
screenTitle = '출고된 장비';
|
||||
} else if (widget.currentRoute == Routes.equipmentRentList) {
|
||||
screenTitle = '대여된 장비';
|
||||
}
|
||||
final int totalCount = _controller.equipments.length;
|
||||
final List<UnifiedEquipment> filteredEquipments =
|
||||
_appliedSearchKeyword.isEmpty
|
||||
? _controller.equipments
|
||||
: _controller.equipments.where((e) {
|
||||
final keyword = _appliedSearchKeyword.toLowerCase();
|
||||
// 모든 주요 필드에서 검색
|
||||
return [
|
||||
e.equipment.manufacturer,
|
||||
e.equipment.name,
|
||||
e.equipment.category,
|
||||
e.equipment.subCategory,
|
||||
e.equipment.subSubCategory,
|
||||
e.equipment.serialNumber ?? '',
|
||||
e.equipment.barcode ?? '',
|
||||
e.equipment.remark ?? '',
|
||||
e.equipment.warrantyLicense ?? '',
|
||||
e.notes ?? '',
|
||||
].any((field) => field.toLowerCase().contains(keyword));
|
||||
}).toList();
|
||||
final int filteredCount = filteredEquipments.length;
|
||||
final int startIndex = (_currentPage - 1) * _pageSize;
|
||||
final int endIndex =
|
||||
(startIndex + _pageSize) > filteredCount
|
||||
? filteredCount
|
||||
: (startIndex + _pageSize);
|
||||
final pagedEquipments = filteredEquipments.sublist(startIndex, endIndex);
|
||||
|
||||
// 선택된 장비 개수
|
||||
final int selectedCount = _controller.getSelectedEquipmentCount();
|
||||
final int selectedInCount = _controller.getSelectedInStockCount();
|
||||
final int selectedOutCount = _controller.getSelectedEquipmentCountByStatus(
|
||||
EquipmentStatus.out,
|
||||
);
|
||||
final int selectedRentCount = _controller.getSelectedEquipmentCountByStatus(
|
||||
EquipmentStatus.rent,
|
||||
);
|
||||
|
||||
return MainLayout(
|
||||
title: screenTitle,
|
||||
currentRoute: widget.currentRoute,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_showDetailedColumns ? Icons.view_column : Icons.view_compact,
|
||||
color: Colors.grey,
|
||||
),
|
||||
tooltip: _showDetailedColumns ? '간소화된 보기' : '상세 보기',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showDetailedColumns = !_showDetailedColumns;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
});
|
||||
},
|
||||
color: Colors.grey,
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
width: maxContentWidth,
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text(
|
||||
screenTitle,
|
||||
style: AppThemeTailwind.headingStyle,
|
||||
),
|
||||
),
|
||||
if (selectedCount > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'$selectedCount개 선택됨',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (widget.currentRoute == Routes.equipmentInList)
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed:
|
||||
selectedInCount > 0 ? _handleOutEquipment : null,
|
||||
icon: const Icon(
|
||||
Icons.exit_to_app,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'출고',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
disabledBackgroundColor: Colors.blue.withOpacity(0.5),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.equipmentInAdd,
|
||||
);
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
_currentPage = 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: const Text(
|
||||
'입고',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: '장비 검색',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchKeyword = value;
|
||||
});
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
setState(() {
|
||||
_appliedSearchKeyword = value;
|
||||
_currentPage = 1;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
tooltip: '검색',
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_appliedSearchKeyword = _searchKeyword;
|
||||
_currentPage = 1;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 출고 목록 화면일 때 버튼들
|
||||
if (widget.currentRoute == Routes.equipmentOutList)
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed:
|
||||
selectedOutCount > 0
|
||||
? () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('재입고 기능은 준비 중입니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(
|
||||
Icons.assignment_return,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'재입고',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
disabledBackgroundColor: Colors.green.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed:
|
||||
selectedOutCount > 0
|
||||
? () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('수리 요청 기능은 준비 중입니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.build, color: Colors.white),
|
||||
label: const Text(
|
||||
'수리 요청',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.orange,
|
||||
disabledBackgroundColor: Colors.orange.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 대여 목록 화면일 때 버튼들
|
||||
if (widget.currentRoute == Routes.equipmentRentList)
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed:
|
||||
selectedRentCount > 0
|
||||
? () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('대여 반납 기능은 준비 중입니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(
|
||||
Icons.keyboard_return,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'반납',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
disabledBackgroundColor: Colors.green.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed:
|
||||
selectedRentCount > 0
|
||||
? () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('대여 연장 기능은 준비 중입니다.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.date_range, color: Colors.white),
|
||||
label: const Text(
|
||||
'연장',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
disabledBackgroundColor: Colors.blue.withOpacity(0.5),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child:
|
||||
pagedEquipments.isEmpty
|
||||
? const Center(child: Text('장비 정보가 없습니다.'))
|
||||
: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: maxContentWidth,
|
||||
),
|
||||
child: EquipmentTable(
|
||||
equipments: pagedEquipments,
|
||||
selectedEquipmentIds:
|
||||
_controller.selectedEquipmentIds,
|
||||
showDetailedColumns: _showDetailedColumns,
|
||||
onEquipmentSelected: _onEquipmentSelected,
|
||||
getOutEquipmentInfo:
|
||||
_controller.getOutEquipmentInfo,
|
||||
buildCategoryWithTooltip: _buildCategoryWithTooltip,
|
||||
// 수정 버튼 동작: 입고 폼(수정 모드)로 이동
|
||||
onEdit: (id, status) async {
|
||||
if (status == EquipmentStatus.in_) {
|
||||
final result = await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.equipmentInEdit,
|
||||
arguments: id,
|
||||
);
|
||||
if (result == true) {
|
||||
setState(() {
|
||||
_controller.loadData();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 출고/대여 등은 별도 폼으로 이동 필요시 구현
|
||||
}
|
||||
},
|
||||
// 삭제 버튼 동작: 삭제 다이얼로그 및 삭제 처리
|
||||
onDelete: (id, status) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('삭제 확인'),
|
||||
content: const Text('이 장비 정보를 삭제하시겠습니까?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
// 입고/출고 상태에 따라 삭제 처리
|
||||
if (status ==
|
||||
EquipmentStatus.in_) {
|
||||
MockDataService()
|
||||
.deleteEquipmentIn(id);
|
||||
} else if (status ==
|
||||
EquipmentStatus.out) {
|
||||
MockDataService()
|
||||
.deleteEquipmentOut(id);
|
||||
}
|
||||
_controller.loadData();
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('삭제'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
getSelectedInStockCount:
|
||||
_controller.getSelectedInStockCount,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (totalCount > _pageSize)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Pagination(
|
||||
totalCount: filteredCount,
|
||||
currentPage: _currentPage,
|
||||
pageSize: _pageSize,
|
||||
onPageChanged: (page) {
|
||||
setState(() {
|
||||
_currentPage = page;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
805
lib/screens/equipment/equipment_out_form.dart
Normal file
805
lib/screens/equipment/equipment_out_form.dart
Normal file
@@ -0,0 +1,805 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/models/company_model.dart';
|
||||
import 'package:superport/models/address_model.dart';
|
||||
import 'package:superport/screens/common/custom_widgets.dart';
|
||||
import 'package:superport/screens/common/theme_tailwind.dart';
|
||||
import 'package:superport/services/mock_data_service.dart';
|
||||
import 'package:superport/screens/equipment/controllers/equipment_out_form_controller.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_card.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||
import 'package:superport/screens/common/widgets/remark_input.dart';
|
||||
|
||||
class EquipmentOutFormScreen extends StatefulWidget {
|
||||
final int? equipmentOutId;
|
||||
final Equipment? selectedEquipment;
|
||||
final int? selectedEquipmentInId;
|
||||
final List<Map<String, dynamic>>? selectedEquipments;
|
||||
|
||||
const EquipmentOutFormScreen({
|
||||
Key? key,
|
||||
this.equipmentOutId,
|
||||
this.selectedEquipment,
|
||||
this.selectedEquipmentInId,
|
||||
this.selectedEquipments,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<EquipmentOutFormScreen> createState() => _EquipmentOutFormScreenState();
|
||||
}
|
||||
|
||||
class _EquipmentOutFormScreenState extends State<EquipmentOutFormScreen> {
|
||||
late final EquipmentOutFormController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = EquipmentOutFormController(dataService: MockDataService());
|
||||
_controller.isEditMode = widget.equipmentOutId != null;
|
||||
_controller.equipmentOutId = widget.equipmentOutId;
|
||||
_controller.selectedEquipment = widget.selectedEquipment;
|
||||
_controller.selectedEquipmentInId = widget.selectedEquipmentInId;
|
||||
_controller.selectedEquipments = widget.selectedEquipments;
|
||||
_controller.loadDropdownData();
|
||||
if (_controller.isEditMode) {
|
||||
// 수정 모드: 기존 출고 정보 로드
|
||||
// (이 부분은 실제 서비스에서 컨트롤러에 메서드 추가 필요)
|
||||
} else if (widget.selectedEquipments != null &&
|
||||
widget.selectedEquipments!.isNotEmpty) {
|
||||
// 다중 선택 장비 있음: 별도 초기화 필요시 컨트롤러에서 처리
|
||||
} else if (widget.selectedEquipment != null) {
|
||||
_controller.initializeWithSelectedEquipment(widget.selectedEquipment!);
|
||||
}
|
||||
}
|
||||
|
||||
// 요약 테이블 위젯 - 다중 선택 장비에 대한 요약 테이블
|
||||
Widget _buildSummaryTable() {
|
||||
if (_controller.selectedEquipments == null ||
|
||||
_controller.selectedEquipments!.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// 각 장비별로 전체 폭을 사용하는 리스트로 구현
|
||||
return Container(
|
||||
width: double.infinity, // 전체 폭 사용
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.zero, // margin 제거
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'선택된 장비 목록 (${_controller.selectedEquipments!.length}개)',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 리스트 헤더
|
||||
Row(
|
||||
children: const [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'제조사',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'장비명',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Text(
|
||||
'수량',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'워런티 시작일',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'워런티 종료일',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
// 리스트 본문
|
||||
Column(
|
||||
children: List.generate(_controller.selectedEquipments!.length, (
|
||||
index,
|
||||
) {
|
||||
final equipmentData = _controller.selectedEquipments![index];
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
// 워런티 날짜를 임시로 저장할 수 있도록 상태를 관리(컨트롤러에 리스트로 추가하거나, 여기서 임시로 관리)
|
||||
// 여기서는 equipment 객체의 필드를 직접 수정(실제 서비스에서는 별도 상태 관리 필요)
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(flex: 2, child: Text(equipment.manufacturer)),
|
||||
Expanded(flex: 2, child: Text(equipment.name)),
|
||||
Expanded(flex: 1, child: Text('${equipment.quantity}')),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
equipment.warrantyStartDate ??
|
||||
DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
equipment.warrantyStartDate = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
_formatDate(equipment.warrantyStartDate),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate:
|
||||
equipment.warrantyEndDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
equipment.warrantyEndDate = picked;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
_formatDate(equipment.warrantyEndDate),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 날짜 포맷 유틸리티
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '정보 없음';
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 담당자가 없거나 첫 번째 회사에 대한 담당자가 '없음'인 경우 등록 버튼 비활성화 조건
|
||||
final bool canSubmit =
|
||||
_controller.selectedCompanies.isNotEmpty &&
|
||||
_controller.selectedCompanies[0] != null &&
|
||||
_controller.hasManagersPerCompany[0] &&
|
||||
_controller.filteredManagersPerCompany[0].first != '없음';
|
||||
final int totalSelectedEquipments =
|
||||
_controller.selectedEquipments?.length ?? 0;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_controller.isEditMode
|
||||
? '장비 출고 수정'
|
||||
: totalSelectedEquipments > 0
|
||||
? '장비 출고 등록 (${totalSelectedEquipments}개)'
|
||||
: '장비 출고 등록',
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _controller.formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 장비 정보 요약 섹션
|
||||
if (_controller.selectedEquipments != null &&
|
||||
_controller.selectedEquipments!.isNotEmpty)
|
||||
_buildSummaryTable()
|
||||
else if (_controller.selectedEquipment != null)
|
||||
// 단일 장비 요약 카드도 전체 폭으로 맞춤
|
||||
Container(
|
||||
width: double.infinity,
|
||||
child: EquipmentSingleSummaryCard(
|
||||
equipment: _controller.selectedEquipment!,
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
// 요약 카드 아래 라디오 버튼 추가
|
||||
const SizedBox(height: 12),
|
||||
// 전체 폭을 사용하는 라디오 버튼
|
||||
Container(width: double.infinity, child: _buildOutTypeRadio()),
|
||||
const SizedBox(height: 16),
|
||||
// 출고 정보 입력 섹션 (수정/등록)
|
||||
_buildOutgoingInfoSection(context),
|
||||
// 비고 입력란 추가
|
||||
const SizedBox(height: 16),
|
||||
FormFieldWrapper(
|
||||
label: '비고',
|
||||
isRequired: false,
|
||||
child: RemarkInput(
|
||||
controller: _controller.remarkController,
|
||||
hint: '비고를 입력하세요',
|
||||
minLines: 4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 담당자 없음 경고 메시지
|
||||
if (_controller.selectedCompanies.isNotEmpty &&
|
||||
_controller.selectedCompanies[0] != null &&
|
||||
(!_controller.hasManagersPerCompany[0] ||
|
||||
_controller.filteredManagersPerCompany[0].first ==
|
||||
'없음'))
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.red.shade300),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.warning, color: Colors.red),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'선택한 회사에 등록된 담당자가 없습니다. 담당자를 먼저 등록해야 합니다.',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 저장 버튼
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
canSubmit
|
||||
? () {
|
||||
// 각 회사별 담당자를 첫 번째 항목으로 설정
|
||||
for (
|
||||
int i = 0;
|
||||
i < _controller.selectedCompanies.length;
|
||||
i++
|
||||
) {
|
||||
if (_controller.selectedCompanies[i] != null &&
|
||||
_controller.hasManagersPerCompany[i] &&
|
||||
_controller
|
||||
.filteredManagersPerCompany[i]
|
||||
.isNotEmpty &&
|
||||
_controller
|
||||
.filteredManagersPerCompany[i]
|
||||
.first !=
|
||||
'없음') {
|
||||
_controller.selectedManagersPerCompany[i] =
|
||||
_controller
|
||||
.filteredManagersPerCompany[i]
|
||||
.first;
|
||||
}
|
||||
}
|
||||
|
||||
_controller.saveEquipmentOut(
|
||||
(msg) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(msg),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
(err) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(err),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
style:
|
||||
canSubmit
|
||||
? AppThemeTailwind.primaryButtonStyle
|
||||
: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
foregroundColor: Colors.grey.shade700,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Text(
|
||||
_controller.isEditMode ? '수정하기' : '등록하기',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 출고 정보 입력 섹션 위젯 (등록/수정 공통)
|
||||
Widget _buildOutgoingInfoSection(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('출고 정보', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
// 출고일
|
||||
_buildDateField(
|
||||
context,
|
||||
label: '출고일',
|
||||
date: _controller.outDate,
|
||||
onDateChanged: (picked) {
|
||||
setState(() {
|
||||
_controller.outDate = picked;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// 출고 회사 영역 헤더
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('출고 회사', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.addCompany();
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.add_circle_outline, size: 18),
|
||||
label: const Text('출고 회사 추가'),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// 동적 출고 회사 드롭다운 목록
|
||||
...List.generate(_controller.selectedCompanies.length, (index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _controller.selectedCompanies[index],
|
||||
decoration: InputDecoration(
|
||||
hintText: index == 0 ? '출고할 회사를 선택하세요' : '추가된 출고할 회사를 선택하세요',
|
||||
// 이전 드롭다운에 값이 선택되지 않았으면 비활성화
|
||||
enabled:
|
||||
index == 0 ||
|
||||
_controller.selectedCompanies[index - 1] != null,
|
||||
),
|
||||
items:
|
||||
_controller.availableCompaniesPerDropdown[index]
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: _buildCompanyDropdownItem(item),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
validator: (value) {
|
||||
if (index == 0 && (value == null || value.isEmpty)) {
|
||||
return '출고 회사를 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged:
|
||||
(index == 0 ||
|
||||
_controller.selectedCompanies[index - 1] != null)
|
||||
? (value) {
|
||||
setState(() {
|
||||
_controller.selectedCompanies[index] = value;
|
||||
_controller.filterManagersByCompanyAtIndex(
|
||||
value,
|
||||
index,
|
||||
);
|
||||
_controller.updateAvailableCompanies();
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// 각 회사별 담당자 선택 목록
|
||||
...List.generate(_controller.selectedCompanies.length, (index) {
|
||||
// 회사가 선택된 경우에만 담당자 표시
|
||||
if (_controller.selectedCompanies[index] != null) {
|
||||
// 회사 정보 가져오기
|
||||
final companyInfo = _controller.companiesWithBranches.firstWhere(
|
||||
(info) => info.name == _controller.selectedCompanies[index],
|
||||
orElse:
|
||||
() => CompanyBranchInfo(
|
||||
id: 0,
|
||||
name: _controller.selectedCompanies[index]!,
|
||||
originalName: _controller.selectedCompanies[index]!,
|
||||
isMainCompany: true,
|
||||
companyId: 0,
|
||||
branchId: null,
|
||||
),
|
||||
);
|
||||
|
||||
// 실제 회사/지점 정보를 ID로 가져오기
|
||||
Company? company;
|
||||
Branch? branch;
|
||||
|
||||
if (companyInfo.companyId != null) {
|
||||
company = _controller.dataService.getCompanyById(
|
||||
companyInfo.companyId!,
|
||||
);
|
||||
if (!companyInfo.isMainCompany &&
|
||||
companyInfo.branchId != null &&
|
||||
company != null) {
|
||||
final branches = company.branches;
|
||||
if (branches != null) {
|
||||
branch = branches.firstWhere(
|
||||
(b) => b.id == companyInfo.branchId,
|
||||
orElse:
|
||||
() => Branch(
|
||||
companyId: companyInfo.companyId!,
|
||||
name: companyInfo.originalName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'담당자 정보 (${_controller.selectedCompanies[index]})',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 15,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child:
|
||||
company != null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 본사/지점 정보 표시
|
||||
if (companyInfo.isMainCompany &&
|
||||
company.contactName != null &&
|
||||
company.contactName!.isNotEmpty)
|
||||
Text(
|
||||
'${company.contactName} ${company.contactPosition ?? ""} ${company.contactPhone ?? ""} ${company.contactEmail ?? ""}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
),
|
||||
if (!companyInfo.isMainCompany &&
|
||||
branch != null &&
|
||||
branch.contactName != null &&
|
||||
branch.contactName!.isNotEmpty)
|
||||
Text(
|
||||
'${branch.contactName} ${branch.contactPosition ?? ""} ${branch.contactPhone ?? ""} ${branch.contactEmail ?? ""}',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 담당자 목록에서 실제 담당자 정보만 표시하는 부분은 제거
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
'회사 정보를 불러올 수 없습니다.',
|
||||
style: TextStyle(
|
||||
color: Colors.red.shade400,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
|
||||
// 유지 보수(라이센스) 선택
|
||||
_buildDropdownField(
|
||||
label: '유지 보수', // 텍스트 변경
|
||||
value: _controller.selectedLicense,
|
||||
items: _controller.licenses,
|
||||
hint: '유지 보수를 선택하세요', // 텍스트 변경
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.selectedLicense = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '유지 보수를 선택해주세요'; // 텍스트 변경
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 날짜 선택 필드 위젯
|
||||
Widget _buildDateField(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required DateTime date,
|
||||
required ValueChanged<DateTime> onDateChanged,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: date,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null && picked != date) {
|
||||
onDateChanged(picked);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_controller.formatDate(date),
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
),
|
||||
const Icon(Icons.calendar_today, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 드롭다운 필드 위젯
|
||||
Widget _buildDropdownField({
|
||||
required String label,
|
||||
required String? value,
|
||||
required List<String> items,
|
||||
required String hint,
|
||||
required ValueChanged<String?>? onChanged,
|
||||
required String? Function(String?) validator,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 4),
|
||||
DropdownButtonFormField<String>(
|
||||
value: value,
|
||||
decoration: InputDecoration(hintText: hint),
|
||||
items:
|
||||
items
|
||||
.map(
|
||||
(item) => DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
validator: validator,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 회사 이름을 표시하는 위젯 (지점 포함)
|
||||
Widget _buildCompanyDropdownItem(String item) {
|
||||
final TextStyle defaultStyle = TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
);
|
||||
|
||||
// 컨트롤러에서 해당 항목에 대한 정보 확인
|
||||
final companyInfoList =
|
||||
_controller.companiesWithBranches
|
||||
.where((info) => info.name == item)
|
||||
.toList();
|
||||
|
||||
// 회사 정보가 존재하고 지점인 경우
|
||||
if (companyInfoList.isNotEmpty && !companyInfoList[0].isMainCompany) {
|
||||
final companyInfo = companyInfoList[0];
|
||||
final parentCompanyName = companyInfo.parentCompanyName ?? '';
|
||||
final branchName = companyInfo.displayName ?? companyInfo.originalName;
|
||||
|
||||
// Row 대신 RichText 사용 - 지점 표시
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: defaultStyle, // 기본 스타일 설정
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.subdirectory_arrow_right,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
TextSpan(text: ' ', style: defaultStyle),
|
||||
TextSpan(
|
||||
text: parentCompanyName, // 회사명
|
||||
style: defaultStyle,
|
||||
),
|
||||
TextSpan(text: ' ', style: defaultStyle),
|
||||
TextSpan(
|
||||
text: branchName, // 지점명
|
||||
style: const TextStyle(
|
||||
color: Colors.indigo,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
}
|
||||
|
||||
// 일반 회사명 (본사)
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: defaultStyle, // 기본 스타일 설정
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Icon(Icons.business, size: 16, color: Colors.black54),
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
),
|
||||
TextSpan(text: ' ', style: defaultStyle),
|
||||
TextSpan(
|
||||
text: item,
|
||||
style: defaultStyle.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
}
|
||||
|
||||
// 회사 ID에 따른 담당자 정보를 가져와 표시하는 위젯 목록 생성
|
||||
List<Widget> _getUsersForCompany(CompanyBranchInfo companyInfo) {
|
||||
final List<Widget> userWidgets = [];
|
||||
|
||||
// 판교지점 특별 처리
|
||||
if (companyInfo.originalName == "판교지점" &&
|
||||
companyInfo.parentCompanyName == "LG전자") {
|
||||
userWidgets.add(
|
||||
Text(
|
||||
'정수진 사원 010-4567-8901 jung.soojin@lg.com',
|
||||
style: AppThemeTailwind.bodyStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return userWidgets;
|
||||
}
|
||||
|
||||
// 출고/대여/폐기 라디오 버튼 위젯
|
||||
Widget _buildOutTypeRadio() {
|
||||
// 출고 유형 리스트
|
||||
final List<String> outTypes = ['출고', '대여', '폐기'];
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children:
|
||||
outTypes.map((type) {
|
||||
return Row(
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: type,
|
||||
groupValue: _controller.outType, // 컨트롤러에서 현재 선택값 관리
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_controller.outType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
Text(type),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
172
lib/screens/equipment/widgets/autocomplete_text_field.dart
Normal file
172
lib/screens/equipment/widgets/autocomplete_text_field.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 자동완성 텍스트 필드 위젯
|
||||
///
|
||||
/// 입력, 드롭다운, 포커스, 필터링, 선택 기능을 모두 포함한다.
|
||||
class AutocompleteTextField extends StatefulWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final List<String> items;
|
||||
final bool isRequired;
|
||||
final String hintText;
|
||||
final void Function(String) onChanged;
|
||||
final void Function(String) onSelected;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
const AutocompleteTextField({
|
||||
Key? key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
required this.onSelected,
|
||||
this.isRequired = false,
|
||||
this.hintText = '',
|
||||
this.focusNode,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AutocompleteTextField> createState() => _AutocompleteTextFieldState();
|
||||
}
|
||||
|
||||
class _AutocompleteTextFieldState extends State<AutocompleteTextField> {
|
||||
late final TextEditingController _controller;
|
||||
late final FocusNode _focusNode;
|
||||
late List<String> _filteredItems;
|
||||
bool _showDropdown = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.value);
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_filteredItems = List.from(widget.items);
|
||||
_controller.addListener(_onTextChanged);
|
||||
_focusNode.addListener(() {
|
||||
setState(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_showDropdown = _filteredItems.isNotEmpty;
|
||||
} else {
|
||||
_showDropdown = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant AutocompleteTextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.value != _controller.text) {
|
||||
_controller.text = widget.value;
|
||||
}
|
||||
if (widget.items != oldWidget.items) {
|
||||
_filteredItems = List.from(widget.items);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.focusNode == null) {
|
||||
_focusNode.dispose();
|
||||
}
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 입력값 변경 시 필터링
|
||||
void _onTextChanged() {
|
||||
final text = _controller.text;
|
||||
setState(() {
|
||||
if (text.isEmpty) {
|
||||
_filteredItems = List.from(widget.items);
|
||||
} else {
|
||||
_filteredItems =
|
||||
widget.items
|
||||
.where(
|
||||
(item) => item.toLowerCase().contains(text.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
// 시작 부분이 일치하는 항목 우선 정렬
|
||||
_filteredItems.sort((a, b) {
|
||||
bool aStartsWith = a.toLowerCase().startsWith(text.toLowerCase());
|
||||
bool bStartsWith = b.toLowerCase().startsWith(text.toLowerCase());
|
||||
if (aStartsWith && !bStartsWith) return -1;
|
||||
if (!aStartsWith && bStartsWith) return 1;
|
||||
return a.compareTo(b);
|
||||
});
|
||||
}
|
||||
_showDropdown = _filteredItems.isNotEmpty && _focusNode.hasFocus;
|
||||
widget.onChanged(text);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleSelect(String value) {
|
||||
setState(() {
|
||||
_controller.text = value;
|
||||
_showDropdown = false;
|
||||
});
|
||||
widget.onSelected(value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: widget.hintText,
|
||||
),
|
||||
validator: (value) {
|
||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||
return '${widget.label}을(를) 입력해주세요';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
widget.onSelected(value ?? '');
|
||||
},
|
||||
),
|
||||
if (_showDropdown)
|
||||
Positioned(
|
||||
top: 50,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
return InkWell(
|
||||
onTap: () => _handleSelect(_filteredItems[index]),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(_filteredItems[index]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/screens/equipment/widgets/equipment_out_info.dart
Normal file
48
lib/screens/equipment/widgets/equipment_out_info.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 출고 정보(회사, 담당자, 라이센스 등)를 아이콘과 함께 표시하는 위젯
|
||||
class EquipmentOutInfoIcon extends StatelessWidget {
|
||||
final String infoType; // company, manager, license 등
|
||||
final String text;
|
||||
|
||||
const EquipmentOutInfoIcon({
|
||||
super.key,
|
||||
required this.infoType,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// infoType에 따라 아이콘 결정
|
||||
IconData iconData;
|
||||
switch (infoType) {
|
||||
case 'company':
|
||||
iconData = Icons.business;
|
||||
break;
|
||||
case 'manager':
|
||||
iconData = Icons.person;
|
||||
break;
|
||||
case 'license':
|
||||
iconData = Icons.book;
|
||||
break;
|
||||
default:
|
||||
iconData = Icons.info;
|
||||
}
|
||||
|
||||
// 아이콘과 텍스트를 Row로 표시
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(iconData, size: 14, color: Colors.grey[700]),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[800]),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/screens/equipment/widgets/equipment_status_chip.dart
Normal file
61
lib/screens/equipment/widgets/equipment_status_chip.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/utils/constants.dart';
|
||||
|
||||
// 장비 상태에 따라 칩(Chip) 위젯을 반환하는 함수형 위젯
|
||||
class EquipmentStatusChip extends StatelessWidget {
|
||||
final String status;
|
||||
|
||||
const EquipmentStatusChip({super.key, required this.status});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 상태별 칩 색상 및 텍스트 지정
|
||||
Color backgroundColor;
|
||||
String statusText;
|
||||
|
||||
switch (status) {
|
||||
case EquipmentStatus.in_:
|
||||
backgroundColor = Colors.green;
|
||||
statusText = '입고';
|
||||
break;
|
||||
case EquipmentStatus.out:
|
||||
backgroundColor = Colors.orange;
|
||||
statusText = '출고';
|
||||
break;
|
||||
case EquipmentStatus.rent:
|
||||
backgroundColor = Colors.blue;
|
||||
statusText = '대여';
|
||||
break;
|
||||
case EquipmentStatus.repair:
|
||||
backgroundColor = Colors.blue;
|
||||
statusText = '수리중';
|
||||
break;
|
||||
case EquipmentStatus.damaged:
|
||||
backgroundColor = Colors.red;
|
||||
statusText = '손상';
|
||||
break;
|
||||
case EquipmentStatus.lost:
|
||||
backgroundColor = Colors.purple;
|
||||
statusText = '분실';
|
||||
break;
|
||||
case EquipmentStatus.etc:
|
||||
backgroundColor = Colors.grey;
|
||||
statusText = '기타';
|
||||
break;
|
||||
default:
|
||||
backgroundColor = Colors.grey;
|
||||
statusText = '알 수 없음';
|
||||
}
|
||||
|
||||
// 칩 위젯 반환
|
||||
return Chip(
|
||||
label: Text(
|
||||
statusText,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
backgroundColor: backgroundColor,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
155
lib/screens/equipment/widgets/equipment_summary_card.dart
Normal file
155
lib/screens/equipment/widgets/equipment_summary_card.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_summary_row.dart';
|
||||
|
||||
// 다중 선택 장비 요약 카드
|
||||
class EquipmentMultiSummaryCard extends StatelessWidget {
|
||||
final List<Map<String, dynamic>> selectedEquipments;
|
||||
const EquipmentMultiSummaryCard({
|
||||
super.key,
|
||||
required this.selectedEquipments,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
'선택된 장비 목록 (${selectedEquipments.length}개)',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
...selectedEquipments.map((equipmentData) {
|
||||
final equipment = equipmentData['equipment'] as Equipment;
|
||||
return EquipmentSingleSummaryCard(equipment: equipment);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 단일 장비 요약 카드
|
||||
class EquipmentSingleSummaryCard extends StatelessWidget {
|
||||
final Equipment equipment;
|
||||
const EquipmentSingleSummaryCard({super.key, required this.equipment});
|
||||
|
||||
// 날짜 포맷 유틸리티
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '정보 없음';
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 3,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
equipment.name.isNotEmpty ? equipment.name : '이름 없음',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.blue.shade300),
|
||||
),
|
||||
child: Text(
|
||||
'수량: ${equipment.quantity}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.blue.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(thickness: 1.5),
|
||||
EquipmentSummaryRow(
|
||||
label: '제조사',
|
||||
value:
|
||||
equipment.manufacturer.isNotEmpty
|
||||
? equipment.manufacturer
|
||||
: '정보 없음',
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '카테고리',
|
||||
value:
|
||||
equipment.category.isNotEmpty
|
||||
? '${equipment.category} > ${equipment.subCategory} > ${equipment.subSubCategory}'
|
||||
: '정보 없음',
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '시리얼 번호',
|
||||
value:
|
||||
(equipment.serialNumber != null &&
|
||||
equipment.serialNumber!.isNotEmpty)
|
||||
? equipment.serialNumber!
|
||||
: '정보 없음',
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '출고 수량',
|
||||
value: equipment.quantity.toString(),
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '입고일',
|
||||
value: _formatDate(equipment.inDate),
|
||||
),
|
||||
// 워런티 정보 추가
|
||||
if (equipment.warrantyLicense != null &&
|
||||
equipment.warrantyLicense!.isNotEmpty)
|
||||
EquipmentSummaryRow(
|
||||
label: '워런티 라이센스',
|
||||
value: equipment.warrantyLicense!,
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '워런티 시작일',
|
||||
value: _formatDate(equipment.warrantyStartDate),
|
||||
),
|
||||
EquipmentSummaryRow(
|
||||
label: '워런티 종료일',
|
||||
value: _formatDate(equipment.warrantyEndDate),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
41
lib/screens/equipment/widgets/equipment_summary_row.dart
Normal file
41
lib/screens/equipment/widgets/equipment_summary_row.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// 장비 요약 정보 행 위젯 (SRP, 재사용성)
|
||||
class EquipmentSummaryRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const EquipmentSummaryRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 110,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: value == '정보 없음' ? Colors.grey.shade600 : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
236
lib/screens/equipment/widgets/equipment_table.dart
Normal file
236
lib/screens/equipment/widgets/equipment_table.dart
Normal file
@@ -0,0 +1,236 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:superport/models/equipment_unified_model.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_status_chip.dart';
|
||||
import 'package:superport/screens/equipment/widgets/equipment_out_info.dart';
|
||||
import 'package:superport/utils/equipment_display_helper.dart';
|
||||
|
||||
// 장비 목록 테이블 위젯 (SRP, 재사용성 강화)
|
||||
class EquipmentTable extends StatelessWidget {
|
||||
final List<UnifiedEquipment> equipments;
|
||||
final Set<String> selectedEquipmentIds;
|
||||
final bool showDetailedColumns;
|
||||
final void Function(int? id, String status, bool? isSelected)
|
||||
onEquipmentSelected;
|
||||
final String Function(int equipmentId, String infoType) getOutEquipmentInfo;
|
||||
final Widget Function(UnifiedEquipment equipment) buildCategoryWithTooltip;
|
||||
final void Function(int id, String status) onEdit;
|
||||
final void Function(int id, String status) onDelete;
|
||||
final int Function() getSelectedInStockCount;
|
||||
|
||||
const EquipmentTable({
|
||||
super.key,
|
||||
required this.equipments,
|
||||
required this.selectedEquipmentIds,
|
||||
required this.showDetailedColumns,
|
||||
required this.onEquipmentSelected,
|
||||
required this.getOutEquipmentInfo,
|
||||
required this.buildCategoryWithTooltip,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.getSelectedInStockCount,
|
||||
});
|
||||
|
||||
// 출고 정보(간소화 모드) 위젯
|
||||
Widget _buildCompactOutInfo(int equipmentId) {
|
||||
final company = getOutEquipmentInfo(equipmentId, 'company');
|
||||
final manager = getOutEquipmentInfo(equipmentId, 'manager');
|
||||
final license = getOutEquipmentInfo(equipmentId, 'license');
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
EquipmentOutInfoIcon(infoType: 'company', text: company),
|
||||
const SizedBox(height: 2),
|
||||
EquipmentOutInfoIcon(infoType: 'manager', text: manager),
|
||||
const SizedBox(height: 2),
|
||||
EquipmentOutInfoIcon(infoType: 'license', text: license),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 카테고리 툴팁 위젯 (UI만 담당, 축약 표기 적용)
|
||||
Widget _buildCategoryWithTooltip(UnifiedEquipment equipment) {
|
||||
// 한글 라벨로 표기
|
||||
final fullCategory =
|
||||
'대분류: ${equipment.equipment.category} / 중분류: ${equipment.equipment.subCategory} / 소분류: ${equipment.equipment.subSubCategory}';
|
||||
final shortCategory = [
|
||||
_shortenCategory(equipment.equipment.category),
|
||||
_shortenCategory(equipment.equipment.subCategory),
|
||||
_shortenCategory(equipment.equipment.subSubCategory),
|
||||
].join(' > ');
|
||||
return Tooltip(message: fullCategory, child: Text(shortCategory));
|
||||
}
|
||||
|
||||
// 카테고리 축약 표기 함수 (예: 컴...)
|
||||
String _shortenCategory(String category) {
|
||||
if (category.length <= 2) return category;
|
||||
return category.substring(0, 2) + '...';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DataTable(
|
||||
headingRowHeight: 48,
|
||||
dataRowMinHeight: 48,
|
||||
dataRowMaxHeight: 60,
|
||||
columnSpacing: 10,
|
||||
horizontalMargin: 16,
|
||||
columns: [
|
||||
const DataColumn(label: SizedBox(width: 32, child: Text('선택'))),
|
||||
const DataColumn(label: SizedBox(width: 32, child: Text('번호'))),
|
||||
if (showDetailedColumns)
|
||||
const DataColumn(label: SizedBox(width: 60, child: Text('제조사'))),
|
||||
const DataColumn(label: SizedBox(width: 90, child: Text('장비명'))),
|
||||
if (showDetailedColumns)
|
||||
const DataColumn(label: SizedBox(width: 110, child: Text('분류'))),
|
||||
if (showDetailedColumns)
|
||||
const DataColumn(label: SizedBox(width: 60, child: Text('장비 유형'))),
|
||||
if (showDetailedColumns)
|
||||
const DataColumn(label: SizedBox(width: 70, child: Text('시리얼번호'))),
|
||||
const DataColumn(label: SizedBox(width: 38, child: Text('수량'))),
|
||||
const DataColumn(label: SizedBox(width: 80, child: Text('변경 일자'))),
|
||||
const DataColumn(label: SizedBox(width: 44, child: Text('상태'))),
|
||||
if (showDetailedColumns) ...[
|
||||
const DataColumn(label: SizedBox(width: 90, child: Text('출고 회사'))),
|
||||
const DataColumn(label: SizedBox(width: 60, child: Text('담당자'))),
|
||||
const DataColumn(label: SizedBox(width: 60, child: Text('라이센스'))),
|
||||
] else
|
||||
const DataColumn(label: SizedBox(width: 110, child: Text('출고 정보'))),
|
||||
const DataColumn(label: SizedBox(width: 60, child: Text('관리'))),
|
||||
],
|
||||
rows:
|
||||
equipments.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final equipment = entry.value;
|
||||
final bool isInStock = equipment.status == 'I';
|
||||
final bool isOutStock = equipment.status == 'O';
|
||||
return DataRow(
|
||||
color: MaterialStateProperty.resolveWith<Color?>(
|
||||
(Set<MaterialState> states) =>
|
||||
index % 2 == 0 ? Colors.grey[50] : null,
|
||||
),
|
||||
cells: [
|
||||
DataCell(
|
||||
Checkbox(
|
||||
value: selectedEquipmentIds.contains(
|
||||
'${equipment.id}:${equipment.status}',
|
||||
),
|
||||
onChanged:
|
||||
(isSelected) => onEquipmentSelected(
|
||||
equipment.id,
|
||||
equipment.status,
|
||||
isSelected,
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
DataCell(Text('${index + 1}')),
|
||||
if (showDetailedColumns)
|
||||
DataCell(
|
||||
Text(
|
||||
EquipmentDisplayHelper.formatManufacturer(
|
||||
equipment.equipment.manufacturer,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
EquipmentDisplayHelper.formatEquipmentName(
|
||||
equipment.equipment.name,
|
||||
),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (showDetailedColumns)
|
||||
DataCell(buildCategoryWithTooltip(equipment)),
|
||||
if (showDetailedColumns)
|
||||
DataCell(
|
||||
Text(
|
||||
equipment.status == 'I' &&
|
||||
equipment is UnifiedEquipment &&
|
||||
equipment.type != null
|
||||
? equipment.type!
|
||||
: '-',
|
||||
),
|
||||
),
|
||||
if (showDetailedColumns)
|
||||
DataCell(
|
||||
Text(
|
||||
EquipmentDisplayHelper.formatSerialNumber(
|
||||
equipment.equipment.serialNumber,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
'${equipment.equipment.quantity}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(EquipmentDisplayHelper.formatDate(equipment.date)),
|
||||
),
|
||||
DataCell(EquipmentStatusChip(status: equipment.status)),
|
||||
if (showDetailedColumns) ...[
|
||||
DataCell(
|
||||
Text(
|
||||
isOutStock
|
||||
? getOutEquipmentInfo(equipment.id!, 'company')
|
||||
: '-',
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
isOutStock
|
||||
? getOutEquipmentInfo(equipment.id!, 'manager')
|
||||
: '-',
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
isOutStock
|
||||
? getOutEquipmentInfo(equipment.id!, 'license')
|
||||
: '-',
|
||||
),
|
||||
),
|
||||
] else
|
||||
DataCell(
|
||||
isOutStock
|
||||
? _buildCompactOutInfo(equipment.id!)
|
||||
: const Text('-'),
|
||||
),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.edit,
|
||||
color: Colors.blue,
|
||||
size: 20,
|
||||
),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: const EdgeInsets.all(5),
|
||||
onPressed:
|
||||
() => onEdit(equipment.id!, equipment.status),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: const EdgeInsets.all(5),
|
||||
onPressed:
|
||||
() => onDelete(equipment.id!, equipment.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user