Files
TK-BOM-Project/frontend/src/components/bom/materials/FittingMaterialsView.jsx
hyungi a5bfeec9aa
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: 엑셀 다운로드 방식 개선 - BOM에서 생성한 엑셀을 구매관리에서 다운로드
- 파이프, 피팅, 플랜지, 밸브 카테고리에 새로운 엑셀 업로드 로직 적용
- createExcelBlob 함수로 클라이언트에서 엑셀 생성 후 서버 업로드
- /purchase-request/upload-excel API로 엑셀 파일 서버 저장
- 구매관리 페이지에서 원본 엑셀 파일 다운로드 가능
- 가스켓, 볼트, 서포트는 추후 개선 시 적용 예정

배포 버전: index-5e5aa4a4.js
2025-10-16 14:53:22 +09:00

788 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { exportMaterialsToExcel, createExcelBlob } from '../../../utils/excelExport';
import api from '../../../api';
import { FilterableHeader, MaterialTable } from '../shared';
const FittingMaterialsView = ({
materials,
selectedMaterials,
setSelectedMaterials,
userRequirements,
setUserRequirements,
purchasedMaterials,
onPurchasedMaterialsUpdate,
fileId,
jobNo,
user,
onNavigate
}) => {
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
const [columnFilters, setColumnFilters] = useState({});
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
// 니플 끝단 정보 추출 (기존 로직 복원)
const extractNippleEndInfo = (description) => {
const descUpper = description.toUpperCase();
// 니플 끝단 패턴들 (기존 NewMaterialsPage와 동일)
const endPatterns = {
'PBE': 'PBE', // Plain Both End
'BBE': 'BBE', // Bevel Both End
'POE': 'POE', // Plain One End
'BOE': 'BOE', // Bevel One End
'TOE': 'TOE', // Thread One End
'SW X NPT': 'SW×NPT', // Socket Weld x NPT
'SW X SW': 'SW×SW', // Socket Weld x Socket Weld
'NPT X NPT': 'NPT×NPT', // NPT x NPT
'BOTH END THREADED': 'B.E.T',
'B.E.T': 'B.E.T',
'ONE END THREADED': 'O.E.T',
'O.E.T': 'O.E.T',
'THREADED': 'THD'
};
for (const [pattern, display] of Object.entries(endPatterns)) {
if (descUpper.includes(pattern)) {
return display;
}
}
return '';
};
// 피팅 정보 파싱 (기존 상세 로직 복원)
const parseFittingInfo = (material) => {
const fittingDetails = material.fitting_details || {};
const classificationDetails = material.classification_details || {};
// 개선된 분류기 결과 우선 사용
const fittingTypeInfo = classificationDetails.fitting_type || {};
const scheduleInfo = classificationDetails.schedule_info || {};
// 기존 필드와 새 필드 통합
const fittingType = fittingTypeInfo.type || fittingDetails.fitting_type || '';
const fittingSubtype = fittingTypeInfo.subtype || fittingDetails.fitting_subtype || '';
const mainSchedule = scheduleInfo.main_schedule || fittingDetails.schedule || '';
const redSchedule = scheduleInfo.red_schedule || '';
const hasDifferentSchedules = scheduleInfo.has_different_schedules || false;
const description = material.original_description || '';
// 피팅 타입별 상세 표시
let displayType = '';
// 개선된 분류기 결과 우선 표시
if (fittingType === 'TEE' && fittingSubtype === 'REDUCING') {
displayType = 'TEE REDUCING';
} else if (fittingType === 'REDUCER' && fittingSubtype === 'CONCENTRIC') {
displayType = 'REDUCER CONC';
} else if (fittingType === 'REDUCER' && fittingSubtype === 'ECCENTRIC') {
displayType = 'REDUCER ECC';
} else if (description.toUpperCase().includes('TEE RED')) {
displayType = 'TEE REDUCING';
} else if (description.toUpperCase().includes('RED CONC')) {
displayType = 'REDUCER CONC';
} else if (description.toUpperCase().includes('RED ECC')) {
displayType = 'REDUCER ECC';
} else if (description.toUpperCase().includes('CAP')) {
if (description.includes('NPT(F)')) {
displayType = 'CAP NPT(F)';
} else if (description.includes('SW')) {
displayType = 'CAP SW';
} else if (description.includes('BW')) {
displayType = 'CAP BW';
} else {
displayType = 'CAP';
}
} else if (description.toUpperCase().includes('PLUG')) {
if (description.toUpperCase().includes('HEX')) {
if (description.includes('NPT(M)')) {
displayType = 'HEX PLUG NPT(M)';
} else {
displayType = 'HEX PLUG';
}
} else if (description.includes('NPT(M)')) {
displayType = 'PLUG NPT(M)';
} else if (description.includes('NPT')) {
displayType = 'PLUG NPT';
} else {
displayType = 'PLUG';
}
} else if (fittingType === 'NIPPLE') {
const length = fittingDetails.length_mm || fittingDetails.avg_length_mm;
const endInfo = extractNippleEndInfo(description);
let nippleType = 'NIPPLE';
if (length) nippleType += ` ${length}mm`;
if (endInfo) nippleType += ` ${endInfo}`;
displayType = nippleType;
} else if (fittingType === 'ELBOW') {
let elbowDetails = [];
// 각도 정보 추출
if (fittingSubtype.includes('90DEG') || description.includes('90') || description.includes('90°')) {
elbowDetails.push('90°');
} else if (fittingSubtype.includes('45DEG') || description.includes('45') || description.includes('45°')) {
elbowDetails.push('45°');
}
// 반경 정보 추출 (Long Radius / Short Radius)
if (fittingSubtype.includes('LONG_RADIUS') || description.toUpperCase().includes('LR') || description.toUpperCase().includes('LONG RADIUS')) {
elbowDetails.push('LR');
} else if (fittingSubtype.includes('SHORT_RADIUS') || description.toUpperCase().includes('SR') || description.toUpperCase().includes('SHORT RADIUS')) {
elbowDetails.push('SR');
}
// 연결 방식
if (description.includes('SW')) {
elbowDetails.push('SW');
} else if (description.includes('BW')) {
elbowDetails.push('BW');
}
// 기본값 설정 (각도가 없으면 90도로 가정)
if (!elbowDetails.some(detail => detail.includes('°'))) {
elbowDetails.unshift('90°');
}
displayType = `ELBOW ${elbowDetails.join(' ')}`.trim();
} else if (fittingType === 'TEE') {
// TEE 타입과 연결 방식 상세 표시
let teeDetails = [];
// 등경/축소 타입
if (fittingSubtype === 'EQUAL' || description.toUpperCase().includes('TEE EQ')) {
teeDetails.push('EQ');
} else if (fittingSubtype === 'REDUCING' || description.toUpperCase().includes('TEE RED')) {
teeDetails.push('RED');
}
// 연결 방식
if (description.includes('SW')) {
teeDetails.push('SW');
} else if (description.includes('BW')) {
teeDetails.push('BW');
}
displayType = `TEE ${teeDetails.join(' ')}`.trim();
} else if (fittingType === 'REDUCER') {
const reducerType = fittingSubtype === 'CONCENTRIC' ? 'CONC' : fittingSubtype === 'ECCENTRIC' ? 'ECC' : '';
const sizes = fittingDetails.reduced_size ? `${material.size_spec}×${fittingDetails.reduced_size}` : material.size_spec;
displayType = `RED ${reducerType} ${sizes}`.trim();
} else if (fittingType === 'SWAGE') {
const swageType = fittingSubtype || '';
displayType = `SWAGE ${swageType}`.trim();
} else if (fittingType === 'OLET') {
const oletSubtype = fittingSubtype || '';
let oletDisplayName = '';
// 백엔드 분류기 결과 우선 사용
switch (oletSubtype) {
case 'SOCKOLET':
oletDisplayName = 'SOCK-O-LET';
break;
case 'WELDOLET':
oletDisplayName = 'WELD-O-LET';
break;
case 'ELLOLET':
oletDisplayName = 'ELL-O-LET';
break;
case 'THREADOLET':
oletDisplayName = 'THREAD-O-LET';
break;
case 'ELBOLET':
oletDisplayName = 'ELB-O-LET';
break;
case 'NIPOLET':
oletDisplayName = 'NIP-O-LET';
break;
case 'COUPOLET':
oletDisplayName = 'COUP-O-LET';
break;
default:
// 백엔드 분류가 없으면 description에서 직접 추출
const upperDesc = description.toUpperCase();
if (upperDesc.includes('SOCK-O-LET') || upperDesc.includes('SOCKOLET')) {
oletDisplayName = 'SOCK-O-LET';
} else if (upperDesc.includes('WELD-O-LET') || upperDesc.includes('WELDOLET')) {
oletDisplayName = 'WELD-O-LET';
} else if (upperDesc.includes('ELL-O-LET') || upperDesc.includes('ELLOLET')) {
oletDisplayName = 'ELL-O-LET';
} else if (upperDesc.includes('THREAD-O-LET') || upperDesc.includes('THREADOLET')) {
oletDisplayName = 'THREAD-O-LET';
} else if (upperDesc.includes('ELB-O-LET') || upperDesc.includes('ELBOLET')) {
oletDisplayName = 'ELB-O-LET';
} else if (upperDesc.includes('NIP-O-LET') || upperDesc.includes('NIPOLET')) {
oletDisplayName = 'NIP-O-LET';
} else if (upperDesc.includes('COUP-O-LET') || upperDesc.includes('COUPOLET')) {
oletDisplayName = 'COUP-O-LET';
} else {
oletDisplayName = 'OLET';
}
}
displayType = oletDisplayName;
} else if (!displayType) {
displayType = fittingType || 'FITTING';
}
// 압력 등급과 스케줄 추출 (기존 NewMaterialsPage 로직)
let pressure = '-';
let schedule = '-';
// 압력 등급 찾기 (3000LB, 6000LB 등) - 소켓웰드 피팅에 특히 중요
const pressureMatch = description.match(/(\d+)LB/i);
if (pressureMatch) {
pressure = `${pressureMatch[1]}LB`;
}
// 소켓웰드 피팅의 경우 압력 등급이 더 중요함
if (description.includes('SW') && !pressureMatch) {
// SW 피팅인데 압력이 명시되지 않은 경우 기본값 설정
if (description.includes('3000') || description.includes('3K')) {
pressure = '3000LB';
} else if (description.includes('6000') || description.includes('6K')) {
pressure = '6000LB';
}
}
// 스케줄 표시 (분리 스케줄 지원) - 개선된 로직
// 레듀싱 자재인지 확인
const isReducingFitting = displayType.includes('REDUCING') || displayType.includes('RED') ||
description.toUpperCase().includes('RED') ||
description.toUpperCase().includes('REDUCING');
if (hasDifferentSchedules && mainSchedule && redSchedule) {
schedule = `${mainSchedule}×${redSchedule}`;
} else if (isReducingFitting && mainSchedule && mainSchedule !== 'UNKNOWN') {
// 레듀싱 자재는 같은 스케줄이라도 명시적으로 표시
schedule = `${mainSchedule}×${mainSchedule}`;
} else if (mainSchedule && mainSchedule !== 'UNKNOWN') {
schedule = mainSchedule;
} else {
// Description에서 스케줄 추출 - 더 강력한 패턴 매칭
const schedulePatterns = [
/SCH\s*(\d+S?)/i, // SCH 40, SCH 80S
/SCHEDULE\s*(\d+S?)/i, // SCHEDULE 40
/스케줄\s*(\d+S?)/i, // 스케줄 40
/(\d+S?)\s*SCH/i, // 40 SCH (역순)
/SCH\.?\s*(\d+S?)/i, // SCH.40
/SCH\s*(\d+S?)\s*[xX×]\s*SCH\s*(\d+S?)/i // SCH 40 x SCH 80
];
for (const pattern of schedulePatterns) {
const match = description.match(pattern);
if (match) {
if (match.length > 2) {
// 분리 스케줄 패턴 (SCH 40 x SCH 80)
schedule = `SCH ${match[1]}×SCH ${match[2]}`;
} else {
const scheduleNum = match[1];
if (isReducingFitting) {
// 레듀싱 자재는 같은 스케줄이라도 명시적으로 표시
schedule = `SCH ${scheduleNum}×SCH ${scheduleNum}`;
} else {
schedule = `SCH ${scheduleNum}`;
}
}
break;
}
}
// 여전히 찾지 못했다면 더 넓은 패턴 시도
if (schedule === '-') {
const broadPatterns = [
/\b(\d+)\s*LB/i, // 압력 등급에서 유추
/\b(40|80|120|160)\b/i, // 일반적인 스케줄 숫자
/\b(10|20|30|40|60|80|100|120|140|160)\b/i // 모든 표준 스케줄
];
for (const pattern of broadPatterns) {
const match = description.match(pattern);
if (match) {
const num = match[1];
// 압력 등급이 아닌 경우만 스케줄로 간주
if (!description.includes(`${num}LB`)) {
if (isReducingFitting) {
// 레듀싱 자재는 같은 스케줄이라도 명시적으로 표시
schedule = `SCH ${num}×SCH ${num}`;
} else {
schedule = `SCH ${num}`;
}
break;
}
}
}
}
}
return {
type: 'FITTING',
subtype: displayType,
size: material.size_spec || '-',
pressure: pressure,
schedule: schedule,
grade: material.full_material_grade || material.material_grade || '-',
quantity: Math.round(material.quantity || 0),
unit: '개',
isFitting: true
};
};
// 정렬 처리
const handleSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') {
direction = 'desc';
}
setSortConfig({ key, direction });
};
// 필터링된 및 정렬된 자재 목록
const getFilteredAndSortedMaterials = () => {
let filtered = materials.filter(material => {
return Object.entries(columnFilters).every(([key, filterValue]) => {
if (!filterValue) return true;
const info = parseFittingInfo(material);
const value = info[key]?.toString().toLowerCase() || '';
return value.includes(filterValue.toLowerCase());
});
});
if (sortConfig.key) {
filtered.sort((a, b) => {
const aInfo = parseFittingInfo(a);
const bInfo = parseFittingInfo(b);
const aValue = aInfo[sortConfig.key] || '';
const bValue = bInfo[sortConfig.key] || '';
if (sortConfig.direction === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
}
return filtered;
};
// 전체 선택/해제 (구매신청된 자재 제외)
const handleSelectAll = () => {
const filteredMaterials = getFilteredAndSortedMaterials();
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
if (selectedMaterials.size === selectableMaterials.length) {
setSelectedMaterials(new Set());
} else {
setSelectedMaterials(new Set(selectableMaterials.map(m => m.id)));
}
};
// 개별 선택 (구매신청된 자재는 선택 불가)
const handleMaterialSelect = (materialId) => {
if (purchasedMaterials.has(materialId)) {
return; // 구매신청된 자재는 선택 불가
}
const newSelected = new Set(selectedMaterials);
if (newSelected.has(materialId)) {
newSelected.delete(materialId);
} else {
newSelected.add(materialId);
}
setSelectedMaterials(newSelected);
};
// 엑셀 내보내기
const handleExportToExcel = async () => {
const selectedMaterialsData = materials.filter(m => selectedMaterials.has(m.id));
if (selectedMaterialsData.length === 0) {
alert('내보낼 자재를 선택해주세요.');
return;
}
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
const excelFileName = `FITTING_Materials_${timestamp}.xlsx`;
const dataWithRequirements = selectedMaterialsData.map(material => ({
...material,
user_requirement: userRequirements[material.id] || ''
}));
try {
console.log('🔄 피팅 엑셀 내보내기 시작 - 새로운 방식');
// 1. 먼저 클라이언트에서 엑셀 파일 생성
console.log('📊 엑셀 Blob 생성 중...', dataWithRequirements.length, '개 자료');
const excelBlob = await createExcelBlob(dataWithRequirements, excelFileName, {
category: 'FITTING',
filename: excelFileName,
uploadDate: new Date().toLocaleDateString()
});
console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes');
// 2. 구매신청 생성
const allMaterialIds = selectedMaterialsData.map(m => m.id);
const response = await api.post('/purchase-request/create', {
file_id: fileId,
job_no: jobNo,
category: 'FITTING',
material_ids: allMaterialIds,
materials_data: dataWithRequirements.map(m => ({
material_id: m.id,
description: m.original_description,
category: m.classified_category,
size: m.size_inch || m.size_spec,
schedule: m.schedule,
material_grade: m.material_grade || m.full_material_grade,
quantity: m.quantity,
unit: m.unit,
user_requirement: userRequirements[m.id] || ''
}))
});
if (response.data.success) {
console.log(`✅ 구매신청 완료: ${response.data.request_no}, request_id: ${response.data.request_id}`);
// 3. 생성된 엑셀 파일을 서버에 업로드
console.log('📤 서버에 엑셀 파일 업로드 중...');
const formData = new FormData();
formData.append('excel_file', excelBlob, excelFileName);
formData.append('request_id', response.data.request_id);
formData.append('category', 'FITTING');
const uploadResponse = await api.post('/purchase-request/upload-excel', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log('✅ 엑셀 업로드 완료:', uploadResponse.data);
if (onPurchasedMaterialsUpdate) {
onPurchasedMaterialsUpdate(allMaterialIds);
}
}
// 4. 클라이언트 다운로드
const url = window.URL.createObjectURL(excelBlob);
const link = document.createElement('a');
link.href = url;
link.download = excelFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
alert(`구매신청 ${response.data?.request_no || ''}이 생성되고 엑셀 파일이 저장되었습니다.`);
} catch (error) {
console.error('엑셀 저장 또는 구매신청 실패:', error);
// 실패 시에도 클라이언트 다운로드는 진행
exportMaterialsToExcel(dataWithRequirements, excelFileName, {
category: 'FITTING',
filename: excelFileName,
uploadDate: new Date().toLocaleDateString()
});
alert('엑셀 파일은 다운로드되었지만 구매신청 생성에 실패했습니다.');
}
// 선택 해제
setSelectedMaterials(new Set());
};
const filteredMaterials = getFilteredAndSortedMaterials();
// 필터 헤더 컴포넌트
const FilterableHeader = ({ sortKey, filterKey, children }) => (
<div className="filterable-header" style={{ position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span
onClick={() => handleSort(sortKey)}
style={{ cursor: 'pointer', flex: 1 }}
>
{children}
{sortConfig.key === sortKey && (
<span style={{ marginLeft: '4px' }}>
{sortConfig.direction === 'asc' ? '↑' : '↓'}
</span>
)}
</span>
<button
onClick={() => setShowFilterDropdown(showFilterDropdown === filterKey ? null : filterKey)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '2px',
fontSize: '12px',
color: '#6b7280'
}}
>
🔍
</button>
</div>
{showFilterDropdown === filterKey && (
<div style={{
position: 'absolute',
top: '100%',
left: 0,
background: 'white',
border: '1px solid #e2e8f0',
borderRadius: '6px',
padding: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
zIndex: 1000,
minWidth: '150px'
}}>
<input
type="text"
placeholder={`Filter ${children}...`}
value={columnFilters[filterKey] || ''}
onChange={(e) => setColumnFilters({
...columnFilters,
[filterKey]: e.target.value
})}
style={{
width: '100%',
padding: '4px 8px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '12px'
}}
autoFocus
/>
</div>
)}
</div>
);
return (
<div style={{ padding: '32px' }}>
{/* 헤더 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h3 style={{
fontSize: '24px',
fontWeight: '700',
color: '#0f172a',
margin: '0 0 8px 0'
}}>
Fitting Materials
</h3>
<p style={{
fontSize: '14px',
color: '#64748b',
margin: 0
}}>
{filteredMaterials.length} items {selectedMaterials.size} selected
</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={handleSelectAll}
style={{
background: 'white',
color: '#6b7280',
border: '1px solid #d1d5db',
borderRadius: '8px',
padding: '10px 16px',
cursor: 'pointer',
fontSize: '14px',
fontWeight: '500'
}}
>
{selectedMaterials.size === filteredMaterials.length ? 'Deselect All' : 'Select All'}
</button>
<button
onClick={handleExportToExcel}
disabled={selectedMaterials.size === 0}
style={{
background: selectedMaterials.size > 0 ? 'linear-gradient(135deg, #10b981 0%, #059669 100%)' : '#e5e7eb',
color: selectedMaterials.size > 0 ? 'white' : '#9ca3af',
border: 'none',
borderRadius: '8px',
padding: '10px 16px',
cursor: selectedMaterials.size > 0 ? 'pointer' : 'not-allowed',
fontSize: '14px',
fontWeight: '500'
}}
>
Export to Excel ({selectedMaterials.size})
</button>
</div>
</div>
{/* 테이블 */}
<div style={{
background: 'white',
borderRadius: '12px',
overflow: 'auto',
maxHeight: '600px',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
}}>
<div style={{ minWidth: '1380px' }}>
{/* 헤더 */}
<div style={{
display: 'grid',
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 200px',
gap: '16px',
padding: '16px',
background: '#f8fafc',
borderBottom: '1px solid #e2e8f0',
fontSize: '14px',
fontWeight: '600',
color: '#374151',
textAlign: 'center'
}}>
<div>
<input
type="checkbox"
checked={(() => {
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
return selectedMaterials.size === selectableMaterials.length && selectableMaterials.length > 0;
})()}
onChange={handleSelectAll}
style={{ cursor: 'pointer' }}
/>
</div>
<div>Type</div>
<div>Size</div>
<div>Pressure</div>
<div>Schedule</div>
<div>Material Grade</div>
<div>User Requirements</div>
<div>Purchase Quantity</div>
<div>Additional Request</div>
</div>
{/* 데이터 행들 */}
{filteredMaterials.map((material, index) => {
const info = parseFittingInfo(material);
const isSelected = selectedMaterials.has(material.id);
const isPurchased = purchasedMaterials.has(material.id);
return (
<div
key={material.id}
style={{
display: 'grid',
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 200px',
gap: '16px',
padding: '16px',
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
background: isSelected ? '#eff6ff' : (isPurchased ? '#fef3c7' : 'white'),
transition: 'background 0.15s ease',
textAlign: 'center'
}}
onMouseEnter={(e) => {
if (!isSelected && !isPurchased) {
e.target.style.background = '#f8fafc';
}
}}
onMouseLeave={(e) => {
if (!isSelected && !isPurchased) {
e.target.style.background = 'white';
}
}}
>
<div>
<input
type="checkbox"
checked={isSelected}
onChange={() => handleMaterialSelect(material.id)}
disabled={isPurchased}
style={{
cursor: isPurchased ? 'not-allowed' : 'pointer',
opacity: isPurchased ? 0.5 : 1
}}
/>
</div>
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
{info.subtype}
{isPurchased && (
<span style={{
marginLeft: '8px',
padding: '2px 6px',
background: '#fbbf24',
color: '#92400e',
borderRadius: '4px',
fontSize: '10px',
fontWeight: '500'
}}>
PURCHASED
</span>
)}
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{info.size}
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{info.pressure}
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{info.schedule}
</div>
<div style={{ fontSize: '14px', color: '#1f2937' }}>
{info.grade}
</div>
<div style={{
fontSize: '14px',
color: '#1f2937',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{material.user_requirements?.join(', ') || '-'}
</div>
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
{info.quantity} {info.unit}
</div>
<div>
<input
type="text"
value={userRequirements[material.id] || ''}
onChange={(e) => setUserRequirements({
...userRequirements,
[material.id]: e.target.value
})}
placeholder="Enter additional request..."
style={{
width: '100%',
padding: '6px 8px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '12px'
}}
/>
</div>
</div>
);
})}
</div>
</div>
{filteredMaterials.length === 0 && (
<div style={{
textAlign: 'center',
padding: '60px 20px',
color: '#64748b'
}}>
<div style={{ fontSize: '18px', fontWeight: '600', marginBottom: '8px' }}>
No Fitting Materials Found
</div>
<div style={{ fontSize: '14px' }}>
{Object.keys(columnFilters).some(key => columnFilters[key])
? 'Try adjusting your filters'
: 'No fitting materials available in this BOM'}
</div>
</div>
)}
</div>
);
};
export default FittingMaterialsView;