343 lines
14 KiB
TypeScript
343 lines
14 KiB
TypeScript
import React, { useState } from 'react'
|
||
import {
|
||
FaPlus,
|
||
FaSearch,
|
||
FaEdit,
|
||
FaTrash,
|
||
FaBox,
|
||
FaCodeBranch,
|
||
FaEye,
|
||
FaCopy,
|
||
FaExclamationTriangle,
|
||
FaClock,
|
||
} from 'react-icons/fa'
|
||
import { MrpBOM, MrpBOMComponent, MrpBOMOperation } from '../../../types/mrp'
|
||
import BOMFormModal from './BOMFormModal'
|
||
import { getBOMTypeColor, getBOMTypeText } from '../../../utils/erp'
|
||
import { mockBOMs } from '../../../mocks/mockBOMs'
|
||
import { Container } from '@/components/shared'
|
||
|
||
const BOMManagement: React.FC = () => {
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
const [showModal, setShowModal] = useState(false)
|
||
const [editingBOM, setEditingBOM] = useState<MrpBOM | null>(null)
|
||
const [selectedBOM, setSelectedBOM] = useState<MrpBOM | null>(null)
|
||
|
||
// Mock data - replace with actual API calls
|
||
const [boms, setBoms] = useState<MrpBOM[]>(mockBOMs)
|
||
|
||
const filteredBOMs = boms.filter(
|
||
(bom) =>
|
||
bom.bomCode.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
bom.materialId.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
bom.version.toLowerCase().includes(searchTerm.toLowerCase()),
|
||
)
|
||
|
||
const getTotalOperationTime = (operations: MrpBOMOperation[]) => {
|
||
return operations.reduce((total, op) => total + op.setupTime + op.runTime, 0)
|
||
}
|
||
|
||
const getTotalComponents = (components: MrpBOMComponent[]) => {
|
||
return components.length
|
||
}
|
||
|
||
const getActiveComponents = (components: MrpBOMComponent[]) => {
|
||
return components.filter((comp) => comp.isActive).length
|
||
}
|
||
|
||
const handleEdit = (bom: MrpBOM) => {
|
||
setEditingBOM(bom)
|
||
setShowModal(true)
|
||
}
|
||
|
||
const handleAddNew = () => {
|
||
setEditingBOM(null)
|
||
setShowModal(true)
|
||
}
|
||
|
||
const handleViewDetails = (bom: MrpBOM) => {
|
||
setSelectedBOM(bom)
|
||
|
||
console.log(bom)
|
||
}
|
||
|
||
const handleCopy = (bom: MrpBOM) => {
|
||
console.log('Copying BOM:', bom.bomCode)
|
||
// Implementation for copying BOM
|
||
}
|
||
|
||
const handleSave = (bom: MrpBOM) => {
|
||
setBoms((prev) => {
|
||
const existing = prev.find((b) => b.id === bom.id)
|
||
if (existing) return prev.map((b) => (b.id === bom.id ? bom : b))
|
||
return [...prev, { ...bom, id: bom.id || String(Date.now()) }]
|
||
})
|
||
setShowModal(false)
|
||
}
|
||
|
||
return (
|
||
<Container>
|
||
<div className="space-y-2">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-2xl font-bold text-gray-900">Ürün Ağaçları (BOM)</h2>
|
||
<p className="text-gray-600">Ürün bileşenlerini ve üretim operasyonlarını yönetin</p>
|
||
</div>
|
||
<button
|
||
onClick={handleAddNew}
|
||
className="bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 flex items-center space-x-2"
|
||
>
|
||
<FaPlus className="w-4 h-4" />
|
||
<span>Yeni BOM</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Search Bar */}
|
||
<div className="relative">
|
||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||
<input
|
||
type="text"
|
||
placeholder="BOM ara..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full pl-10 pr-4 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* BOM List */}
|
||
<div className="space-y-3 pt-2">
|
||
<h3 className="text-lg font-semibold text-gray-900">BOM Listesi</h3>
|
||
{filteredBOMs.map((bom) => (
|
||
<div
|
||
key={bom.id}
|
||
className="bg-white rounded-lg shadow-md border border-gray-200 p-3 hover:shadow-lg transition-shadow"
|
||
>
|
||
<div className="flex items-start justify-between mb-2">
|
||
<div className="flex-1">
|
||
<div className="flex items-center space-x-2 mb-2">
|
||
<FaCodeBranch className="w-4 h-4 text-gray-600" />
|
||
<h4 className="text-lg font-semibold text-gray-900">{bom.bomCode}</h4>
|
||
<span
|
||
className={`px-2 py-1 rounded-full text-xs font-medium ${getBOMTypeColor(
|
||
bom.bomType,
|
||
)}`}
|
||
>
|
||
{getBOMTypeText(bom.bomType)}
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mb-0.5">
|
||
Malzeme: {bom.material?.code} - {bom.material?.name} - v{bom.version}
|
||
</p>
|
||
<p className="text-sm text-gray-500">Temel Miktar: {bom.baseQuantity}</p>
|
||
</div>
|
||
<div className="flex space-x-1">
|
||
<button
|
||
onClick={() => handleViewDetails(bom)}
|
||
className="p-2 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-md transition-colors"
|
||
title="Detayları Görüntüle"
|
||
>
|
||
<FaEye className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleCopy(bom)}
|
||
className="p-2 text-gray-400 hover:text-purple-600 hover:bg-purple-50 rounded-md transition-colors"
|
||
title="Kopyala"
|
||
>
|
||
<FaCopy className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleEdit(bom)}
|
||
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors"
|
||
title="Düzenle"
|
||
>
|
||
<FaEdit className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||
title="Sil"
|
||
>
|
||
<FaTrash className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3 text-sm mb-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-500 flex items-center">
|
||
<FaBox className="w-4 h-4 mr-1" />
|
||
Bileşenler
|
||
</span>
|
||
<span className="font-medium">
|
||
{getActiveComponents(bom.components)}/{getTotalComponents(bom.components)}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-500 flex items-center">
|
||
<FaClock className="w-4 h-4 mr-1" />
|
||
Toplam Süre
|
||
</span>
|
||
<span className="font-medium">{getTotalOperationTime(bom.operations)} dk</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-500">Geçerlilik</span>
|
||
<span className="font-medium">{bom.validFrom.toLocaleDateString('tr-TR')}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status and Warnings */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex space-x-2">
|
||
<span
|
||
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||
bom.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||
}`}
|
||
>
|
||
{bom.isActive ? 'Aktif' : 'Pasif'}
|
||
</span>
|
||
{bom.validTo && new Date() > bom.validTo && (
|
||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 flex items-center">
|
||
<FaExclamationTriangle className="w-3 h-3 mr-1" />
|
||
Süresi Dolmuş
|
||
</span>
|
||
)}
|
||
</div>
|
||
<span className="text-xs text-gray-400">
|
||
{bom.lastModificationTime.toLocaleDateString('tr-TR')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{filteredBOMs.length === 0 && (
|
||
<div className="text-center py-8">
|
||
<FaCodeBranch className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">BOM bulunamadı</h3>
|
||
<p className="text-gray-500">
|
||
Arama kriterlerinizi değiştirin veya yeni bir BOM ekleyin.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* BOM Details */}
|
||
<div className="space-y-4 pt-2">
|
||
<h3 className="text-lg font-semibold text-gray-900">BOM Detayları</h3>
|
||
{selectedBOM ? (
|
||
<div className="bg-white rounded-lg shadow-md border border-gray-200 p-3">
|
||
<div className="mb-3">
|
||
<h4 className="font-semibold text-gray-900 mb-2">{selectedBOM.bomCode}</h4>
|
||
<div className="grid grid-cols-2 gap-1 text-sm text-gray-600">
|
||
<div>
|
||
Malzeme: {selectedBOM.material?.code} - {selectedBOM.material?.name}
|
||
</div>
|
||
<div>Versiyon: {selectedBOM.version}</div>
|
||
<div>Tip: {getBOMTypeText(selectedBOM.bomType)}</div>
|
||
<div>Temel Miktar: {selectedBOM.baseQuantity}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Components */}
|
||
<div className="mb-3">
|
||
<h5 className="font-medium text-gray-900 mb-2 flex items-center">
|
||
<FaBox className="w-4 h-4 mr-1" />
|
||
Bileşenler ({selectedBOM.components.length})
|
||
</h5>
|
||
<div className="space-y-2">
|
||
{selectedBOM.components.map((component) => (
|
||
<div
|
||
key={component.id}
|
||
className="border border-gray-200 rounded p-1.5 text-sm"
|
||
>
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="font-medium">
|
||
{component.position}. {component.material?.code} -{' '}
|
||
{component.material?.name}
|
||
</span>
|
||
<div className="flex space-x-2">
|
||
{component.isPhantom && (
|
||
<span className="bg-purple-100 text-purple-800 text-xs px-1 rounded-lg">
|
||
Phantom
|
||
</span>
|
||
)}
|
||
<span
|
||
className={`text-xs px-1 rounded ${
|
||
component.isActive
|
||
? 'bg-green-100 text-green-800'
|
||
: 'bg-red-100 text-red-800'
|
||
}`}
|
||
>
|
||
{component.isActive ? 'Aktif' : 'Pasif'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="text-gray-600">
|
||
Miktar: {component.quantity} {component.unitId} | Fire: %
|
||
{component.scrapPercentage}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Operations */}
|
||
<div>
|
||
<h5 className="font-medium text-gray-900 mb-2 flex items-center">
|
||
<FaCodeBranch className="w-4 h-4 mr-1" />
|
||
Operasyonlar ({selectedBOM.operations.length})
|
||
</h5>
|
||
<div className="space-y-2">
|
||
{selectedBOM.operations.map((operation) => (
|
||
<div
|
||
key={operation.id}
|
||
className="border border-gray-200 rounded p-2 text-sm"
|
||
>
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="font-medium">
|
||
{operation.sequence}. {operation.operation?.code} -{' '}
|
||
{operation.operation?.name}
|
||
</span>
|
||
<span
|
||
className={`text-xs px-1 rounded ${
|
||
operation.isActive
|
||
? 'bg-green-100 text-green-800'
|
||
: 'bg-red-100 text-red-800'
|
||
}`}
|
||
>
|
||
{operation.isActive ? 'Aktif' : 'Pasif'}
|
||
</span>
|
||
</div>
|
||
<div className="text-gray-600">
|
||
Hazırlık: {operation.setupTime}dk | İşlem: {operation.runTime}dk | Kuyruk:{' '}
|
||
{operation.queueTime}dk | Taşıma: {operation.moveTime}dk
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="bg-gray-50 rounded-lg p-6 text-center">
|
||
<FaBox className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||
BOM Detaylarını Görüntüle
|
||
</h3>
|
||
<p className="text-gray-500">Detaylarını görmek için sol taraftan bir BOM seçin.</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<BOMFormModal
|
||
open={showModal}
|
||
initial={editingBOM}
|
||
onSave={handleSave}
|
||
onClose={() => setShowModal(false)}
|
||
/>
|
||
</Container>
|
||
)
|
||
}
|
||
|
||
export default BOMManagement
|