Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- frontend/src/pages/revision/ 폴더 완전 삭제 - EnhancedRevisionPage.css 제거 - support_details 저장 시 트랜잭션 오류로 인해 임시로 상세 정보 저장 비활성화 - 리비전 기능 재설계 예정
616 lines
21 KiB
JavaScript
616 lines
21 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
||
import { fetchMaterials } from '../api';
|
||
import api from '../api';
|
||
import {
|
||
PipeMaterialsView,
|
||
FittingMaterialsView,
|
||
FlangeMaterialsView,
|
||
ValveMaterialsView,
|
||
GasketMaterialsView,
|
||
BoltMaterialsView,
|
||
SupportMaterialsView,
|
||
SpecialMaterialsView,
|
||
UnclassifiedMaterialsView
|
||
} from '../components/bom';
|
||
import './BOMManagementPage.css';
|
||
|
||
const BOMManagementPage = ({
|
||
onNavigate,
|
||
selectedProject,
|
||
fileId,
|
||
jobNo,
|
||
bomName,
|
||
revision,
|
||
filename,
|
||
user
|
||
}) => {
|
||
const [materials, setMaterials] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [selectedCategory, setSelectedCategory] = useState('PIPE');
|
||
const [selectedMaterials, setSelectedMaterials] = useState(new Set());
|
||
const [exportHistory, setExportHistory] = useState([]);
|
||
const [availableRevisions, setAvailableRevisions] = useState([]);
|
||
const [currentRevision, setCurrentRevision] = useState(revision || 'Rev.0');
|
||
const [userRequirements, setUserRequirements] = useState({});
|
||
const [purchasedMaterials, setPurchasedMaterials] = useState(new Set());
|
||
const [error, setError] = useState(null);
|
||
|
||
// 리비전 관련 상태
|
||
const [isRevisionMode, setIsRevisionMode] = useState(false);
|
||
const [revisionData, setRevisionData] = useState(null);
|
||
const [previousFileId, setPreviousFileId] = useState(null);
|
||
const [changedMaterials, setChangedMaterials] = useState({});
|
||
|
||
// 자재 업데이트 함수 (브랜드, 사용자 요구사항 등)
|
||
const updateMaterial = (materialId, updates) => {
|
||
setMaterials(prevMaterials =>
|
||
prevMaterials.map(material =>
|
||
material.id === materialId
|
||
? { ...material, ...updates }
|
||
: material
|
||
)
|
||
);
|
||
};
|
||
|
||
// 카테고리 정의
|
||
const categories = [
|
||
{ key: 'PIPE', label: 'Pipes', color: '#3b82f6' },
|
||
{ key: 'FITTING', label: 'Fittings', color: '#10b981' },
|
||
{ key: 'FLANGE', label: 'Flanges', color: '#f59e0b' },
|
||
{ key: 'VALVE', label: 'Valves', color: '#ef4444' },
|
||
{ key: 'GASKET', label: 'Gaskets', color: '#8b5cf6' },
|
||
{ key: 'BOLT', label: 'Bolts', color: '#6b7280' },
|
||
{ key: 'SUPPORT', label: 'Supports', color: '#f97316' },
|
||
{ key: 'SPECIAL', label: 'Special Items', color: '#ec4899' },
|
||
{ key: 'UNCLASSIFIED', label: 'Unclassified', color: '#64748b' }
|
||
];
|
||
|
||
// 자료 로드 함수들
|
||
const loadMaterials = async (id) => {
|
||
try {
|
||
setLoading(true);
|
||
console.log('🔍 자재 데이터 로딩 중...', {
|
||
file_id: id,
|
||
selectedProject: selectedProject?.job_no || selectedProject?.official_project_code,
|
||
jobNo
|
||
});
|
||
|
||
// 구매신청된 자재 먼저 확인
|
||
const projectJobNo = selectedProject?.job_no || selectedProject?.official_project_code || jobNo;
|
||
await loadPurchasedMaterials(projectJobNo);
|
||
|
||
const response = await fetchMaterials({
|
||
file_id: parseInt(id),
|
||
limit: 10000,
|
||
exclude_requested: false,
|
||
job_no: projectJobNo
|
||
});
|
||
|
||
if (response.data?.materials) {
|
||
const materialsData = response.data.materials;
|
||
console.log(`✅ ${materialsData.length}개 원본 자재 로드 완료`);
|
||
setMaterials(materialsData);
|
||
setError(null);
|
||
} else {
|
||
console.warn('⚠️ 자재 데이터가 없습니다:', response.data);
|
||
setMaterials([]);
|
||
}
|
||
} catch (error) {
|
||
console.error('자재 로드 실패:', error);
|
||
setError('자재 로드에 실패했습니다.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const loadAvailableRevisions = async () => {
|
||
try {
|
||
const response = await api.get('/files/', {
|
||
params: { job_no: jobNo }
|
||
});
|
||
|
||
const allFiles = Array.isArray(response.data) ? response.data : response.data?.files || [];
|
||
const sameBomFiles = allFiles.filter(file =>
|
||
(file.bom_name || file.original_filename) === bomName
|
||
);
|
||
|
||
sameBomFiles.sort((a, b) => {
|
||
const revA = parseInt(a.revision?.replace('Rev.', '') || '0');
|
||
const revB = parseInt(b.revision?.replace('Rev.', '') || '0');
|
||
return revB - revA;
|
||
});
|
||
|
||
setAvailableRevisions(sameBomFiles);
|
||
} catch (error) {
|
||
console.error('리비전 목록 조회 실패:', error);
|
||
}
|
||
};
|
||
|
||
const loadPurchasedMaterials = async (jobNo) => {
|
||
try {
|
||
// 새로운 API로 구매신청된 자재 ID 목록 조회
|
||
const response = await api.get('/purchase-request/requested-materials', {
|
||
params: {
|
||
job_no: jobNo,
|
||
file_id: fileId
|
||
}
|
||
});
|
||
|
||
if (response.data?.requested_material_ids) {
|
||
const purchasedIds = new Set(response.data.requested_material_ids);
|
||
setPurchasedMaterials(purchasedIds);
|
||
console.log(`✅ ${purchasedIds.size}개 구매신청된 자재 ID 로드 완료`);
|
||
}
|
||
} catch (error) {
|
||
console.error('구매신청 자재 조회 실패:', error);
|
||
}
|
||
};
|
||
|
||
const loadUserRequirements = async (fileId) => {
|
||
try {
|
||
const response = await api.get(`/files/${fileId}/user-requirements`);
|
||
if (response.data?.requirements) {
|
||
const reqMap = {};
|
||
response.data.requirements.forEach(req => {
|
||
reqMap[req.material_id] = req.requirement;
|
||
});
|
||
setUserRequirements(reqMap);
|
||
}
|
||
} catch (error) {
|
||
console.error('사용자 요구사항 로드 실패:', error);
|
||
}
|
||
};
|
||
|
||
// 리비전 모드 감지 및 변경된 자재 로드
|
||
const checkAndLoadRevisionData = async () => {
|
||
try {
|
||
// 현재 job_no의 모든 파일 목록 확인
|
||
const filesResponse = await api.get(`/files/list?job_no=${jobNo}`);
|
||
const files = filesResponse.data.files || [];
|
||
|
||
if (files.length > 1) {
|
||
// 파일이 여러 개 있으면 리비전 모드 활성화
|
||
setIsRevisionMode(true);
|
||
|
||
// 파일들을 업로드 날짜순으로 정렬
|
||
const sortedFiles = files.sort((a, b) => new Date(a.upload_date) - new Date(b.upload_date));
|
||
|
||
// 이전 파일 ID 찾기 (현재 파일 이전 버전)
|
||
const currentIndex = sortedFiles.findIndex(file => file.id === parseInt(fileId));
|
||
if (currentIndex > 0) {
|
||
const previousFile = sortedFiles[currentIndex - 1];
|
||
setPreviousFileId(previousFile.id);
|
||
|
||
// 변경된 자재 로드
|
||
await loadChangedMaterials(fileId, previousFile.id);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('리비전 데이터 로드 실패:', error);
|
||
// API 오류 시 리비전 모드 비활성화
|
||
setIsRevisionMode(false);
|
||
}
|
||
};
|
||
|
||
// 변경된 자재 로드
|
||
const loadChangedMaterials = async (currentFileId, previousFileId) => {
|
||
try {
|
||
const response = await api.get(`/simple-revision/changed-materials/${currentFileId}/${previousFileId}`);
|
||
if (response.data.success) {
|
||
setChangedMaterials(response.data.data.changes_by_category || {});
|
||
setRevisionData(response.data.data);
|
||
console.log('✅ 변경된 자재 로드 완료:', response.data.data);
|
||
}
|
||
} catch (error) {
|
||
console.error('변경된 자재 로드 실패:', error);
|
||
}
|
||
};
|
||
|
||
// 초기 로드
|
||
useEffect(() => {
|
||
if (fileId) {
|
||
loadMaterials(fileId);
|
||
loadAvailableRevisions();
|
||
loadUserRequirements(fileId);
|
||
checkAndLoadRevisionData(); // 리비전 데이터 확인
|
||
}
|
||
}, [fileId]);
|
||
|
||
// 자재 로드 후 선택된 카테고리가 유효한지 확인
|
||
useEffect(() => {
|
||
if (materials.length > 0) {
|
||
const availableCategories = categories.filter(category => {
|
||
const count = getCategoryMaterials(category.key).length;
|
||
return count > 0;
|
||
});
|
||
|
||
// 현재 선택된 카테고리에 자재가 없으면 첫 번째 유효한 카테고리로 전환
|
||
const currentCategoryHasMaterials = getCategoryMaterials(selectedCategory).length > 0;
|
||
if (!currentCategoryHasMaterials && availableCategories.length > 0) {
|
||
setSelectedCategory(availableCategories[0].key);
|
||
}
|
||
}
|
||
}, [materials, selectedCategory]);
|
||
|
||
// 카테고리별 자재 필터링 (리비전 모드 지원)
|
||
const getCategoryMaterials = (category) => {
|
||
if (isRevisionMode && changedMaterials[category]) {
|
||
// 리비전 모드: 변경된 자재만 표시
|
||
const changedMaterialIds = changedMaterials[category].changes.map(change => change.material_id);
|
||
return materials.filter(material =>
|
||
(material.classified_category === category || material.category === category) &&
|
||
changedMaterialIds.includes(material.id)
|
||
);
|
||
} else {
|
||
// 일반 모드: 모든 자재 표시
|
||
return materials.filter(material =>
|
||
material.classified_category === category ||
|
||
material.category === category
|
||
);
|
||
}
|
||
};
|
||
|
||
// 리비전 액션 정보 가져오기
|
||
const getRevisionAction = (materialId, category) => {
|
||
if (!isRevisionMode || !changedMaterials[category]) return null;
|
||
|
||
const change = changedMaterials[category].changes.find(c => c.material_id === materialId);
|
||
return change || null;
|
||
};
|
||
|
||
// 카테고리별 컴포넌트 렌더링
|
||
const renderCategoryView = () => {
|
||
const categoryMaterials = getCategoryMaterials(selectedCategory);
|
||
const commonProps = {
|
||
materials: categoryMaterials,
|
||
selectedMaterials,
|
||
setSelectedMaterials,
|
||
userRequirements,
|
||
setUserRequirements,
|
||
purchasedMaterials,
|
||
onPurchasedMaterialsUpdate: (materialIds) => {
|
||
setPurchasedMaterials(prev => {
|
||
const newSet = new Set(prev);
|
||
materialIds.forEach(id => newSet.add(id));
|
||
console.log(`📦 구매신청 자재 추가: 기존 ${prev.size}개 → 신규 ${newSet.size}개`);
|
||
return newSet;
|
||
});
|
||
},
|
||
updateMaterial, // 자재 업데이트 함수 추가
|
||
fileId,
|
||
jobNo,
|
||
user,
|
||
onNavigate,
|
||
// 리비전 관련 props 추가
|
||
isRevisionMode,
|
||
getRevisionAction: (materialId) => getRevisionAction(materialId, selectedCategory),
|
||
revisionData
|
||
};
|
||
|
||
switch (selectedCategory) {
|
||
case 'PIPE':
|
||
return <PipeMaterialsView {...commonProps} />;
|
||
case 'FITTING':
|
||
return <FittingMaterialsView {...commonProps} />;
|
||
case 'FLANGE':
|
||
return <FlangeMaterialsView {...commonProps} />;
|
||
case 'VALVE':
|
||
return <ValveMaterialsView {...commonProps} />;
|
||
case 'GASKET':
|
||
return <GasketMaterialsView {...commonProps} />;
|
||
case 'BOLT':
|
||
return <BoltMaterialsView {...commonProps} />;
|
||
case 'SUPPORT':
|
||
return <SupportMaterialsView {...commonProps} />;
|
||
case 'SPECIAL':
|
||
return <SpecialMaterialsView {...commonProps} />;
|
||
case 'UNCLASSIFIED':
|
||
return <UnclassifiedMaterialsView {...commonProps} />;
|
||
default:
|
||
return <div>카테고리를 선택해주세요.</div>;
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
height: '100vh',
|
||
background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)'
|
||
}}>
|
||
<div style={{ textAlign: 'center' }}>
|
||
<div style={{
|
||
width: '60px',
|
||
height: '60px',
|
||
border: '4px solid #e2e8f0',
|
||
borderTop: '4px solid #3b82f6',
|
||
borderRadius: '50%',
|
||
animation: 'spin 1s linear infinite',
|
||
margin: '0 auto 20px'
|
||
}}></div>
|
||
<div style={{ fontSize: '18px', color: '#64748b', fontWeight: '600' }}>
|
||
Loading Materials...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{
|
||
padding: '40px',
|
||
background: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%)',
|
||
minHeight: '100vh'
|
||
}}>
|
||
{/* 헤더 섹션 */}
|
||
<div style={{
|
||
background: 'rgba(255, 255, 255, 0.95)',
|
||
backdropFilter: 'blur(10px)',
|
||
borderRadius: '20px',
|
||
padding: '32px',
|
||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||
marginBottom: '40px'
|
||
}}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '28px' }}>
|
||
<div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
||
<h2 style={{
|
||
fontSize: '28px',
|
||
fontWeight: '700',
|
||
color: '#0f172a',
|
||
margin: 0,
|
||
letterSpacing: '-0.025em'
|
||
}}>
|
||
BOM Materials Management
|
||
</h2>
|
||
{isRevisionMode && (
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||
color: 'white',
|
||
padding: '6px 12px',
|
||
borderRadius: '8px',
|
||
fontSize: '12px',
|
||
fontWeight: '600',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.05em',
|
||
boxShadow: '0 4px 6px -1px rgba(245, 158, 11, 0.3)'
|
||
}}>
|
||
📊 Revision Mode
|
||
</div>
|
||
)}
|
||
</div>
|
||
<p style={{
|
||
fontSize: '16px',
|
||
color: '#64748b',
|
||
margin: 0,
|
||
fontWeight: '400'
|
||
}}>
|
||
{bomName} - {currentRevision} | Project: {selectedProject?.job_name || jobNo}
|
||
{isRevisionMode && revisionData && (
|
||
<span style={{
|
||
marginLeft: '16px',
|
||
color: '#f59e0b',
|
||
fontWeight: '600'
|
||
}}>
|
||
• {revisionData.total_changed_materials} materials changed
|
||
</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => onNavigate('dashboard')}
|
||
style={{
|
||
background: 'white',
|
||
color: '#6b7280',
|
||
border: '1px solid #d1d5db',
|
||
borderRadius: '12px',
|
||
padding: '12px 20px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
transition: 'all 0.2s ease',
|
||
letterSpacing: '0.025em'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.target.style.background = '#f9fafb';
|
||
e.target.style.borderColor = '#9ca3af';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.target.style.background = 'white';
|
||
e.target.style.borderColor = '#d1d5db';
|
||
}}
|
||
>
|
||
Back to Dashboard
|
||
</button>
|
||
</div>
|
||
|
||
{/* 통계 정보 */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||
gap: '20px',
|
||
marginBottom: '32px'
|
||
}}>
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
|
||
padding: '20px',
|
||
borderRadius: '12px',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: '32px', fontWeight: '700', color: '#1d4ed8', marginBottom: '4px' }}>
|
||
{materials.length}
|
||
</div>
|
||
<div style={{ fontSize: '14px', color: '#1d4ed8', fontWeight: '500' }}>
|
||
Total Materials
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
|
||
padding: '20px',
|
||
borderRadius: '12px',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: '32px', fontWeight: '700', color: '#059669', marginBottom: '4px' }}>
|
||
{getCategoryMaterials(selectedCategory).length}
|
||
</div>
|
||
<div style={{ fontSize: '14px', color: '#059669', fontWeight: '500' }}>
|
||
{selectedCategory} Items
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
||
padding: '20px',
|
||
borderRadius: '12px',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: '32px', fontWeight: '700', color: '#92400e', marginBottom: '4px' }}>
|
||
{selectedMaterials.size}
|
||
</div>
|
||
<div style={{ fontSize: '14px', color: '#92400e', fontWeight: '500' }}>
|
||
Selected
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
|
||
padding: '20px',
|
||
borderRadius: '12px',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: '32px', fontWeight: '700', color: '#dc2626', marginBottom: '4px' }}>
|
||
{purchasedMaterials.size}
|
||
</div>
|
||
<div style={{ fontSize: '14px', color: '#dc2626', fontWeight: '500' }}>
|
||
Purchased
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 카테고리 탭 */}
|
||
<div style={{
|
||
background: 'rgba(255, 255, 255, 0.95)',
|
||
backdropFilter: 'blur(10px)',
|
||
borderRadius: '20px',
|
||
padding: '24px 32px',
|
||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||
marginBottom: '40px'
|
||
}}>
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
|
||
gap: '16px'
|
||
}}>
|
||
{categories
|
||
.filter((category) => {
|
||
const count = getCategoryMaterials(category.key).length;
|
||
return count > 0; // 0개인 카테고리는 숨김
|
||
})
|
||
.map((category) => {
|
||
const isActive = selectedCategory === category.key;
|
||
const count = getCategoryMaterials(category.key).length;
|
||
const hasChanges = isRevisionMode && changedMaterials[category.key];
|
||
|
||
return (
|
||
<button
|
||
key={category.key}
|
||
onClick={() => setSelectedCategory(category.key)}
|
||
style={{
|
||
background: isActive
|
||
? `linear-gradient(135deg, ${category.color} 0%, ${category.color}dd 100%)`
|
||
: hasChanges
|
||
? 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)'
|
||
: 'white',
|
||
color: isActive ? 'white' : hasChanges ? '#92400e' : '#64748b',
|
||
border: isActive ? 'none' : hasChanges ? '2px solid #f59e0b' : '1px solid #e2e8f0',
|
||
borderRadius: '12px',
|
||
padding: '16px 12px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
transition: 'all 0.2s ease',
|
||
textAlign: 'center',
|
||
boxShadow: isActive ? `0 4px 14px 0 ${category.color}39` : hasChanges ? '0 4px 14px 0 rgba(245, 158, 11, 0.3)' : '0 2px 8px rgba(0,0,0,0.05)',
|
||
position: 'relative'
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!isActive) {
|
||
e.target.style.background = '#f8fafc';
|
||
e.target.style.borderColor = '#cbd5e1';
|
||
}
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
if (!isActive) {
|
||
e.target.style.background = 'white';
|
||
e.target.style.borderColor = '#e2e8f0';
|
||
}
|
||
}}
|
||
>
|
||
<div style={{ marginBottom: '4px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '4px' }}>
|
||
{category.label}
|
||
{hasChanges && (
|
||
<div style={{
|
||
width: '8px',
|
||
height: '8px',
|
||
borderRadius: '50%',
|
||
background: isActive ? 'rgba(255,255,255,0.8)' : '#f59e0b'
|
||
}}></div>
|
||
)}
|
||
</div>
|
||
<div style={{
|
||
fontSize: '12px',
|
||
opacity: 0.8,
|
||
fontWeight: '500'
|
||
}}>
|
||
{count} items
|
||
{hasChanges && (
|
||
<span style={{ marginLeft: '4px', fontSize: '10px' }}>
|
||
({changedMaterials[category.key].changed_count} changed)
|
||
</span>
|
||
)}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 카테고리별 컨텐츠 */}
|
||
<div style={{
|
||
background: 'rgba(255, 255, 255, 0.95)',
|
||
backdropFilter: 'blur(10px)',
|
||
borderRadius: '20px',
|
||
boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||
overflow: 'hidden'
|
||
}}>
|
||
{error ? (
|
||
<div style={{
|
||
padding: '60px',
|
||
textAlign: 'center',
|
||
color: '#dc2626'
|
||
}}>
|
||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>⚠️</div>
|
||
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
|
||
Error Loading Materials
|
||
</div>
|
||
<div style={{ fontSize: '14px' }}>
|
||
{error}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
renderCategoryView()
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default BOMManagementPage;
|