3 Commits

Author SHA1 Message Date
4b765aba2e feat: 자산 유형별 UI 최적화 및 자산번호 자동 생성 기능 구현
- CPU/GPU/RAM/HDD 등 부품 유형별 필드 라벨 동적 변경 로직 추가\n- 유형별 불필요한 사양 필드 숨김 처리 및 UI 레이아웃 정교화\n- 서버측 자산번호 생성 API (/api/generate-asset-code) 구현\n- 모달 내 자산번호 자동 생성 버튼 이벤트 연동 및 백엔드 동기화
2026-04-22 10:11:45 +09:00
7247737ce0 merge: 통합 HW 모달 구현 (PC 상세유형 복구 + 전산비품/모바일 확장 통합) 2026-04-21 18:09:13 +09:00
e4d958b5f2 fix: PC 상세 유형(개인PC, 서버) 선택 및 UI 제어 로직 복구 2026-04-21 17:56:29 +09:00
2 changed files with 237 additions and 46 deletions

View File

@@ -323,6 +323,34 @@ app.post('/api/sw-users/batch', async (req, res) => {
} catch (err) { res.status(500).json({ error: err.message }); } } catch (err) { res.status(500).json({ error: err.message }); }
}); });
// 자산번호 자동 생성 API
app.get('/api/generate-asset-code', async (req, res) => {
const { prefix } = req.query;
if (!prefix) return res.status(400).json({ error: 'Prefix is required' });
try {
const tables = ['pc_assets', 'server_assets', 'storage_assets', 'equip_assets', 'mobile_assets'];
let maxNum = 0;
for (const table of tables) {
const [rows] = await pool.query(
`SELECT asset_code FROM ${table} WHERE asset_code LIKE ?`,
[`${prefix}%`]
);
rows.forEach(r => {
const numPart = r.asset_code.replace(prefix, '');
const num = parseInt(numPart);
if (!isNaN(num) && num > maxNum) maxNum = num;
});
}
const nextNum = (maxNum + 1).toString().padStart(3, '0');
res.json({ nextCode: `${prefix}${nextNum}` });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// 초기화 및 서버 기동 // 초기화 및 서버 기동
ensureTables().then(() => { ensureTables().then(() => {
app.listen(PORT, () => { app.listen(PORT, () => {

View File

@@ -49,6 +49,10 @@ const HW_MODAL_HTML = `
<label for="hw-현사용조직">현 사용조직</label> <label for="hw-현사용조직">현 사용조직</label>
<select id="hw-현사용조직">${generateOptionsHTML(ORG_LIST)}</select> <select id="hw-현사용조직">${generateOptionsHTML(ORG_LIST)}</select>
</div> </div>
<div class="form-group" id="hw-이전사용조직-group">
<label for="hw-이전사용조직">이전 사용조직</label>
<input type="text" id="hw-이전사용조직" readonly />
</div>
<div class="form-group"> <div class="form-group">
<label for="hw-유형">유형</label> <label for="hw-유형">유형</label>
<select id="hw-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select> <select id="hw-유형">${generateOptionsHTML(HW_TYPE_LIST)}</select>
@@ -61,12 +65,8 @@ const HW_MODAL_HTML = `
<option value="개인PC">개인PC</option> <option value="개인PC">개인PC</option>
</select> </select>
</div> </div>
<div class="form-group full-width non-server">
<label for="hw-명칭">명칭 / 모델명</label>
<input type="text" id="hw-명칭" />
</div>
<!-- Group 2: 운영 및 상태 (Operation) - 전산비품/모바일 전용 --> <!-- 운영 및 상태 관리 (전산비품/모바일 전용) -->
<div class="form-section-title op-only">운영 및 상태 관리</div> <div class="form-section-title op-only">운영 및 상태 관리</div>
<div class="form-group op-only"> <div class="form-group op-only">
<label for="hw-보관위치">보관위치</label> <label for="hw-보관위치">보관위치</label>
@@ -77,34 +77,70 @@ const HW_MODAL_HTML = `
<select id="hw-현재상태">${generateOptionsHTML(STATUS_LIST)}</select> <select id="hw-현재상태">${generateOptionsHTML(STATUS_LIST)}</select>
</div> </div>
<!-- Group 3: 네트워크 및 서버 정보 (Connectivity) --> <!-- 네트워크 및 서버 정보 -->
<div class="form-section-title server-only">네트워크 정보 (Connectivity)</div> <div class="form-section-title server-only" id="hw-network-title">네트워크 정보 (Connectivity)</div>
<div class="form-group server-only"> <div class="form-group server-only" id="hw-ip-group">
<label for="hw-IP주소">IP 주소 1</label> <label for="hw-IP주소">IP 주소 1</label>
<input type="text" id="hw-IP주소" /> <input type="text" id="hw-IP주소" />
</div> </div>
<div class="form-group server-only"> <div class="form-group server-only" id="hw-ip2-group">
<label for="hw-IP2">IP 주소 2</label>
<input type="text" id="hw-IP2" />
</div>
<div class="form-group server-only" id="hw-remote-group">
<label for="hw-원격접속">원격 도구</label> <label for="hw-원격접속">원격 도구</label>
<input type="text" id="hw-원격접속" /> <input type="text" id="hw-원격접속" />
</div> </div>
<div class="form-group server-only" id="hw-server-id-group">
<label for="hw-서버ID">서버 ID</label>
<input type="text" id="hw-서버ID" />
</div>
<div class="form-group server-only" id="hw-server-pw-group">
<label for="hw-서버PW">서버 PW</label>
<input type="text" id="hw-서버PW" />
</div>
<div class="form-group non-server" id="hw-ip-non-server-group">
<label for="hw-IP주소-non-server">IP 주소</label>
<input type="text" id="hw-IP주소-non-server" />
</div>
<!-- Group 4: 시스템 사양 (Specifications) --> <!-- 시스템 사양 -->
<div class="form-section-title">시스템 사양 (Specifications)</div> <div class="form-section-title" id="hw-spec-title">시스템 사양 (Specifications)</div>
<div class="form-group"> <div class="form-group" id="hw-model-group">
<label for="hw-모델명">모델명</label> <label for="hw-모델명">모델명</label>
<input type="text" id="hw-모델명" /> <input type="text" id="hw-모델명" />
</div> </div>
<div class="form-group"> <div class="form-group" id="hw-os-group">
<label for="hw-OS">운영체제 (OS)</label> <label for="hw-OS">운영체제 (OS)</label>
<input type="text" id="hw-OS" /> <input type="text" id="hw-OS" />
</div> </div>
<div class="form-group full-width"> <div class="form-group" id="hw-cpu-group">
<label for="hw-CPU">CPU 사양</label>
<input type="text" id="hw-CPU" />
</div>
<div class="form-group" id="hw-ram-group">
<label for="hw-RAM">RAM 용량</label>
<input type="text" id="hw-RAM" />
</div>
<div class="form-group" id="hw-ssd1-group">
<label for="hw-SSD1">Storage 1 (SSD/HDD)</label>
<input type="text" id="hw-SSD1" />
</div>
<div class="form-group" id="hw-ssd2-group">
<label for="hw-SSD2">Storage 2 (SSD/HDD)</label>
<input type="text" id="hw-SSD2" />
</div>
<div class="form-group server-only" id="hw-monitoring-group">
<label for="hw-모니터링">모니터링 여부</label>
<input type="text" id="hw-모니터링" />
</div>
<div class="form-group full-width non-server" id="hw-hwspec-group">
<label for="hw-HW사양">사양 상세</label> <label for="hw-HW사양">사양 상세</label>
<textarea id="hw-HW사양" rows="2"></textarea> <textarea id="hw-HW사양" rows="2"></textarea>
</div> </div>
<!-- Group 5: 관리 및 위치 (Location) --> <!-- 설치 위치 및 관리 -->
<div class="form-section-title">설치 위치 및 관리</div> <div class="form-section-title" id="hw-op-title">설치 위치 및 관리</div>
<div class="form-group loc-standard"> <div class="form-group loc-standard">
<label for="hw-위치-빌딩">설치위치 (건물)</label> <label for="hw-위치-빌딩">설치위치 (건물)</label>
<select id="hw-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select> <select id="hw-위치-빌딩">${generateOptionsHTML(Object.keys(LOCATION_DATA))}</select>
@@ -113,8 +149,12 @@ const HW_MODAL_HTML = `
<label for="hw-위치-상세">상세 위치</label> <label for="hw-위치-상세">상세 위치</label>
<select id="hw-위치-상세"><option value="">선택</option></select> <select id="hw-위치-상세"><option value="">선택</option></select>
</div> </div>
<div class="form-group" id="hw-위치-기타-group" style="display:none;">
<label for="hw-위치-기타">직접 입력 (기타)</label>
<input type="text" id="hw-위치-기타" />
</div>
<div class="form-group"> <div class="form-group">
<label for="hw-담당자_정">관리자(정)</label> <label for="hw-담당자_정">담당자(정)</label>
<input type="text" id="hw-담당자_정" /> <input type="text" id="hw-담당자_정" />
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -168,7 +208,7 @@ const HW_MODAL_HTML = `
<input type="date" id="new-hw-log-date" /> <input type="date" id="new-hw-log-date" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>상세 내용</label> <label>변경/분출 내용</label>
<textarea id="new-hw-log-details" rows="3" placeholder="예: [분출] 기술팀 홍길동, [수리] 배터리 교체 등"></textarea> <textarea id="new-hw-log-details" rows="3" placeholder="예: [분출] 기술팀 홍길동, [수리] 배터리 교체 등"></textarea>
</div> </div>
</div> </div>
@@ -202,24 +242,106 @@ function renderHwHistory(assetId: string) {
} }
function applyTypeSpecificUI(type: string) { function applyTypeSpecificUI(type: string) {
const opFields = document.querySelectorAll('.op-only'); const detailPurpose = getFieldValue('hw-상세용도');
const serverFields = document.querySelectorAll('.server-only'); const upperType = (type || '').toUpperCase();
const standardLocFields = document.querySelectorAll('.loc-standard');
const upperType = type.toUpperCase(); const groups: Record<string, HTMLElement | null> = {
// 1. 모바일기기 그룹: 모바일, 태블릿, 노트북 detailPurpose: document.getElementById('hw-상세용도-group'),
const isMobileGroup = ['모바일', '태블릿', '노트북', '휴대폰', '핸드폰'].some(t => type.includes(t)); networkTitle: document.getElementById('hw-network-title'),
// 2. 전산비품 그룹: CPU, RAM, HDD, GPU specTitle: document.getElementById('hw-spec-title'),
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || type.includes('비품'); opTitle: document.getElementById('hw-op-title'),
model: document.getElementById('hw-model-group'),
os: document.getElementById('hw-os-group'),
cpu: document.getElementById('hw-cpu-group'),
ram: document.getElementById('hw-ram-group'),
ssd1: document.getElementById('hw-ssd1-group'),
ssd2: document.getElementById('hw-ssd2-group'),
hwSpec: document.getElementById('hw-hwspec-group'),
monitoring: document.getElementById('hw-monitoring-group')
};
const serverOnly = document.querySelectorAll('.server-only');
const nonServer = document.querySelectorAll('.non-server');
const opOnly = document.querySelectorAll('.op-only');
const standardLoc = document.querySelectorAll('.loc-standard');
// 1. 초기화 (모두 숨김 및 라벨 원복)
serverOnly.forEach(el => (el as HTMLElement).style.display = 'none');
nonServer.forEach(el => (el as HTMLElement).style.display = 'none');
opOnly.forEach(el => (el as HTMLElement).style.display = 'none');
standardLoc.forEach(el => (el as HTMLElement).style.display = 'flex');
Object.values(groups).forEach(g => { if (g) g.style.display = 'none'; });
const osLabel = document.querySelector('label[for="hw-OS"]') as HTMLElement;
const ramLabel = document.querySelector('label[for="hw-RAM"]') as HTMLElement;
const modelLabel = document.querySelector('label[for="hw-모델명"]') as HTMLElement;
if (osLabel) osLabel.innerText = '운영체제 (OS)';
if (ramLabel) ramLabel.innerText = 'RAM 용량';
if (modelLabel) modelLabel.innerText = '모델명';
// 2. 분류 판별
const isMobileGroup = ['모바일', '태블릿', '휴대폰'].some(t => upperType.includes(t));
const isEquipGroup = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => upperType.includes(t)) || upperType.includes('비품');
const isOpType = isMobileGroup || isEquipGroup; const isOpType = isMobileGroup || isEquipGroup;
const isServerType = ['서버', '스토리지', 'NAS', 'DAS'].includes(type); const isPcType = upperType === 'PC' || upperType === '개인PC' || upperType === '노트북';
opFields.forEach(el => (el as HTMLElement).style.display = isOpType ? 'flex' : 'none'); // 3. 레이아웃 적용
serverFields.forEach(el => (el as HTMLElement).style.display = isServerType ? 'flex' : 'none'); if (groups.opTitle) groups.opTitle.style.display = 'flex';
// 전산비품/모바일 그룹은 표준 위치 선택(건물/상세) 숨김 if (isOpType) {
standardLocFields.forEach(el => (el as HTMLElement).style.display = isOpType ? 'none' : 'flex'); opOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
standardLoc.forEach(el => (el as HTMLElement).style.display = 'none');
if (groups.specTitle) groups.specTitle.style.display = 'flex';
if (groups.model) groups.model.style.display = 'flex';
// 특정 부품 유형에 따른 라벨 및 필드 제어
const isCpuGpu = ['CPU', 'GPU'].some(t => upperType.includes(t));
const isRamHdd = ['RAM', 'HDD'].some(t => upperType.includes(t));
if (isCpuGpu) {
if (groups.os && osLabel) {
osLabel.innerText = '출시연월';
groups.os.style.display = 'flex';
}
} else if (isRamHdd) {
if (groups.ram && ramLabel) {
ramLabel.innerText = '용량';
groups.ram.style.display = 'flex';
}
// HDD인 경우 모델명 라벨을 S/N으로 변경
if (upperType.includes('HDD') && modelLabel) {
modelLabel.innerText = 'S/N';
}
} else {
if (groups.hwSpec) groups.hwSpec.style.display = 'flex';
}
}
else if (isPcType) {
if (groups.detailPurpose) groups.detailPurpose.style.display = 'flex';
if (groups.specTitle) groups.specTitle.style.display = 'flex';
if (detailPurpose === '서버') {
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => {
if (groups[k]) groups[k]!.style.display = 'flex';
});
} else {
nonServer.forEach(el => (el as HTMLElement).style.display = 'flex');
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'hwSpec'].forEach(k => {
if (groups[k]) groups[k]!.style.display = 'flex';
});
}
}
else if (upperType.includes('서버') || ['스토리지', 'NAS', 'DAS'].includes(upperType)) {
serverOnly.forEach(el => (el as HTMLElement).style.display = 'flex');
if (groups.networkTitle) groups.networkTitle.style.display = 'flex';
if (groups.specTitle) groups.specTitle.style.display = 'flex';
['model', 'os', 'cpu', 'ram', 'ssd1', 'ssd2', 'monitoring'].forEach(k => {
if (groups[k]) groups[k]!.style.display = 'flex';
});
}
} }
export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') { export function openHwModal(asset: HardwareAsset, mode: 'view' | 'add' = 'view') {
@@ -247,21 +369,31 @@ function fillHwFormData(asset: HardwareAsset) {
setFieldValue('hw-법인', asset.); setFieldValue('hw-법인', asset.);
setFieldValue('hw-자산코드', asset.); setFieldValue('hw-자산코드', asset.);
setFieldValue('hw-현사용조직', asset.); setFieldValue('hw-현사용조직', asset.);
setFieldValue('hw-이전사용조직', asset.);
setFieldValue('hw-상세용도', (asset as any).);
setFieldValue('hw-유형', asset.type); setFieldValue('hw-유형', asset.type);
setFieldValue('hw-모델명', asset.);
setFieldValue('hw-명칭', asset. || asset.); setFieldValue('hw-명칭', asset. || asset.);
setFieldValue('hw-보관위치', asset. || ''); setFieldValue('hw-보관위치', asset. || '');
setFieldValue('hw-현재상태', asset. || '보관중'); setFieldValue('hw-현재상태', asset. || '보관중');
setFieldValue('hw-IP주소', asset.IP주소); setFieldValue('hw-IP주소', asset.IP주소);
setFieldValue('hw-IP2', (asset as any).IP2);
setFieldValue('hw-원격접속', (asset as any).); setFieldValue('hw-원격접속', (asset as any).);
setFieldValue('hw-모델명', asset.); setFieldValue('hw-서버ID', (asset as any).ID);
setFieldValue('hw-서버PW', (asset as any).PW);
setFieldValue('hw-모니터링', (asset as any).);
setFieldValue('hw-OS', asset.OS); setFieldValue('hw-OS', asset.OS);
setFieldValue('hw-CPU', asset.CPU);
setFieldValue('hw-RAM', asset.RAM);
setFieldValue('hw-SSD1', asset.SSD1);
setFieldValue('hw-SSD2', asset.SSD2);
setFieldValue('hw-HW사양', asset.HW사양); setFieldValue('hw-HW사양', asset.HW사양);
setFieldValue('hw-담당자_정', asset._정 || asset.); setFieldValue('hw-담당자_정', asset._정 || asset.);
setFieldValue('hw-구매일', asset.); setFieldValue('hw-구매일', asset.);
setFieldValue('hw-금액', asset.); setFieldValue('hw-금액', asset.);
setFieldValue('hw-비고', asset.); setFieldValue('hw-비고', asset.);
parseAndSetLocation(asset., 'hw-위치-빌딩', 'hw-위치-상세', '', ''); parseAndSetLocation(asset., 'hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
} }
export function initHwModal(onSave: () => void, closeModals: () => void) { export function initHwModal(onSave: () => void, closeModals: () => void) {
@@ -274,11 +406,15 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
const revertBtn = document.getElementById('btn-revert-hw-edit')!; const revertBtn = document.getElementById('btn-revert-hw-edit')!;
const deleteBtn = document.getElementById('btn-delete-hw-asset')!; const deleteBtn = document.getElementById('btn-delete-hw-asset')!;
const typeSelect = document.getElementById('hw-유형') as HTMLSelectElement; const typeSelect = document.getElementById('hw-유형') as HTMLSelectElement;
const detailPurposeSelect = document.getElementById('hw-상세용도') as HTMLSelectElement;
const logAddBtn = document.getElementById('btn-add-hw-log')!; const logAddBtn = document.getElementById('btn-add-hw-log')!;
const logModal = document.getElementById('hw-log-modal')!; const logModal = document.getElementById('hw-log-modal')!;
typeSelect.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value)); [typeSelect, detailPurposeSelect].forEach(el => {
bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', '', ''); el?.addEventListener('change', () => applyTypeSpecificUI(typeSelect.value));
});
bindLocationEvents('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타-group', 'hw-위치-기타');
const closeModalAction = () => { closeModals(); isEditMode = false; }; const closeModalAction = () => { closeModals(); isEditMode = false; };
document.getElementById('btn-close-hw-modal')?.addEventListener('click', closeModalAction); document.getElementById('btn-close-hw-modal')?.addEventListener('click', closeModalAction);
@@ -290,19 +426,43 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
if (currentAsset) fillHwFormData(currentAsset); if (currentAsset) fillHwFormData(currentAsset);
}); });
document.getElementById('btn-generate-hw-code')?.addEventListener('click', async () => {
const typeValue = typeSelect.value;
const purchaseDate = getFieldValue('hw-구매일');
const typeCode = TYPE_PREFIX_MAP[typeValue] || 'ETC';
// 구매일에서 연월(YYMM) 추출 (예: 2026-04-21 -> 2604)
const dateStr = purchaseDate.replace(/[^0-9]/g, '');
if (dateStr.length < 4) {
alert('올바른 구매일(연월)을 입력해주세요. (예: 2026-04-21)');
return;
}
const prefix = `${typeCode}-${dateStr.substring(2, 6)}-`;
try {
const res = await fetch(`http://localhost:3000/api/generate-asset-code?prefix=${prefix}`);
const data = await res.json();
if (data.nextCode) {
setFieldValue('hw-자산코드', data.nextCode);
}
} catch (err) {
console.error('❌ 자산번호 생성 실패:', err);
alert('자산번호 생성에 실패했습니다.');
}
});
saveBtn.addEventListener('click', () => { saveBtn.addEventListener('click', () => {
if (!currentAsset) return; if (!currentAsset) return;
if (!isEditMode) { if (!isEditMode) {
setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' }); setEditLock('hw-asset-form', 'edit', { saveBtnId: 'btn-save-hw-asset', revertBtnId: 'btn-revert-hw-edit' });
isEditMode = true; isEditMode = true;
// 수정 모드 전환 시 현재 유형에 맞춰 UI 강제 갱신
applyTypeSpecificUI(getFieldValue('hw-유형')); applyTypeSpecificUI(getFieldValue('hw-유형'));
return; return;
} }
const type = getFieldValue('hw-유형'); const type = getFieldValue('hw-유형');
const isOpType = ['전산비품', '모바일기기'].includes(type);
const storageLoc = getFieldValue('hw-보관위치'); const storageLoc = getFieldValue('hw-보관위치');
const isOpType = ['CPU', 'RAM', 'HDD', 'GPU'].some(t => type.toUpperCase().includes(t)) || type.includes('비품') || ['모바일', '태블릿', '노트북'].some(t => type.includes(t));
const updated: any = { const updated: any = {
...currentAsset, ...currentAsset,
@@ -310,18 +470,21 @@ export function initHwModal(onSave: () => void, closeModals: () => void) {
자산코드: getFieldValue('hw-자산코드'), 자산코드: getFieldValue('hw-자산코드'),
현사용조직: getFieldValue('hw-현사용조직'), 현사용조직: getFieldValue('hw-현사용조직'),
type: type, type: type,
상세용도: getFieldValue('hw-상세용도'),
명칭: getFieldValue('hw-명칭'), 명칭: getFieldValue('hw-명칭'),
보관위치: storageLoc, 보관위치: storageLoc,
현재상태: getFieldValue('hw-현재상태'), 현재상태: getFieldValue('hw-현재상태'),
IP주소: getFieldValue('hw-IP주소'),
모델명: getFieldValue('hw-모델명'),
OS: getFieldValue('hw-OS'), OS: getFieldValue('hw-OS'),
HW사양: getFieldValue('hw-HW사양'), CPU: getFieldValue('hw-CPU'),
RAM: getFieldValue('hw-RAM'),
SSD1: getFieldValue('hw-SSD1'),
SSD2: getFieldValue('hw-SSD2'),
IP주소: getFieldValue('hw-IP주소') || getFieldValue('hw-IP주소-non-server'),
담당자_정: getFieldValue('hw-담당자_정'), 담당자_정: getFieldValue('hw-담당자_정'),
구매일: getFieldValue('hw-구매일'), 구매일: getFieldValue('hw-구매일'),
금액: getFieldValue('hw-금액'), 금액: getFieldValue('hw-금액'),
비고: getFieldValue('hw-비고'), 비고: getFieldValue('hw-비고'),
위치: isOpType ? storageLoc : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', '') 위치: isOpType ? storageLoc : getCombinedLocation('hw-위치-빌딩', 'hw-위치-상세', 'hw-위치-기타')
}; };
saveHardwareAsset(updated); saveHardwareAsset(updated);