erp-platform/ui/src/views/mrp/components/BOMManagement.tsx
2025-09-17 12:46:58 +03:00

343 lines
14 KiB
TypeScript
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 {
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