import React, { useState, useRef, useMemo } from 'react' import { createPortal } from 'react-dom' import { MovementStatusEnum, MovementTypeEnum } from '../../../types/mm' import { FaSearch, FaPlus, FaEdit, FaTrash, FaBox, FaCheckCircle, FaClock, FaExclamationCircle, FaDownload, FaUpload, FaEye, FaArrowCircleUp, FaSave, FaTimes, FaMapMarkerAlt, FaInfoCircle, FaWarehouse, FaBarcode, FaCalculator, } from 'react-icons/fa' import { mockWarehouses } from '../../../mocks/mockWarehouses' import { mockStockMovements } from '../../../mocks/mockStockMovements' import { mockMaterials } from '../../../mocks/mockMaterials' import { mockEmployees } from '../../../mocks/mockEmployees' import Widget from '../../../components/common/Widget' import { Container } from '@/components/shared' interface StockLocation { id: string warehouseId: string warehouseName: string warehouseCode: string zoneId: string zoneName: string zoneCode: string locationId: string locationName: string locationCode: string lotNumber: string availableQuantity: number requestedQuantity: number unitPrice: number selected?: boolean } interface MaterialRow { id: string materialId: string selectedStockLocations: StockLocation[] } // Separate component for lot search input to prevent re-renders const LotSearchInput = React.memo( ({ value, onSearch }: { value: string; onSearch: (value: string) => void }) => { const inputRef = useRef(null) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { const inputValue = (e.target as HTMLInputElement).value onSearch(inputValue) } } const handleClearSearch = () => { if (inputRef.current) { inputRef.current.value = '' onSearch('') } } return (
{value && ( )}
) }, ) const WarehouseIssue: React.FC = () => { const [searchTerm, setSearchTerm] = useState('') const [statusFilter, setStatusFilter] = useState('') const [selectedMovement, setSelectedMovement] = useState('') const [showIssueForm, setShowIssueForm] = useState(false) const [editingMovement, setEditingMovement] = useState(null) // Form state - Header data for the entire movement const [headerData, setHeaderData] = useState({ movementNumber: '', referenceDocument: '', referenceType: 'Sales Order', reason: '', movementDate: new Date().toISOString().split('T')[0], performedBy: '', approvedBy: '', description: '', status: MovementStatusEnum.Planned, }) // Materials list for the movement - New structure for stock-based selection const [materialsList, setMaterialsList] = useState([]) // Stock location modal state const [showStockModal, setShowStockModal] = useState(false) const [selectedMaterialForStock, setSelectedMaterialForStock] = useState('') const [availableStockLocations, setAvailableStockLocations] = useState([]) const [lotSearchTerm, setLotSearchTerm] = useState('') // Memoized filtered locations to prevent unnecessary re-renders const filteredLocations = useMemo(() => { return availableStockLocations.filter( (location) => lotSearchTerm === '' || location.lotNumber.toLowerCase().includes(lotSearchTerm.toLowerCase()), ) }, [availableStockLocations, lotSearchTerm]) const resetForm = () => { setHeaderData({ movementNumber: '', referenceDocument: '', referenceType: 'Sales Order', reason: '', movementDate: new Date().toISOString().split('T')[0], performedBy: '', approvedBy: '', description: '', status: MovementStatusEnum.Planned, }) setMaterialsList([]) setEditingMovement(null) } const handleHeaderInputChange = (field: string, value: string) => { setHeaderData((prev) => ({ ...prev, [field]: value, })) } const removeMaterial = (materialId: string) => { if (window.confirm('Bu malzemeyi listeden kaldırmak istediğinizden emin misiniz?')) { setMaterialsList((prev) => prev.filter((material) => material.id !== materialId)) } } // Generate mock stock locations for a given material const generateStockLocationsForMaterial = (materialId: string): StockLocation[] => { const material = mockMaterials.find((m) => m.id === materialId) if (!material || !material.totalStock || material.totalStock <= 0) return [] const stockLocations: StockLocation[] = [] let remainingStock = material.totalStock // Generate some random stock distributions across warehouses/zones/locations mockWarehouses.forEach((warehouse) => { if (remainingStock <= 0) return warehouse.zones?.forEach((zone) => { if (remainingStock <= 0) return const locationsInZone = warehouse.locations?.filter((loc) => loc.zoneId === zone.id) || [] locationsInZone.forEach((location) => { if (remainingStock <= 0) return // Random distribution of stock (this would come from actual stock data in real app) const stockAtLocation = Math.min( Math.floor(Math.random() * 100) + 20, // Increased range for more realistic amounts remainingStock, ) if (stockAtLocation > 0) { // Generate 2-4 different lots per location with different dates const lotsCount = Math.floor(Math.random() * 3) + 2 // 2-4 lots // Split stock among lots with realistic distribution const lotDistribution = [] let tempStock = stockAtLocation for (let i = 0; i < lotsCount && tempStock > 5; i++) { if (i === lotsCount - 1) { // Last lot gets remaining stock lotDistribution.push(tempStock) } else { // Distribute randomly but ensure each lot has at least 5 units const maxForThisLot = Math.floor(tempStock * 0.6) const minForThisLot = Math.min(5, tempStock) const lotStock = Math.floor(Math.random() * (maxForThisLot - minForThisLot + 1)) + minForThisLot lotDistribution.push(lotStock) tempStock -= lotStock } } // Create lot entries with different dates and batch numbers lotDistribution.forEach((lotStock, index) => { if (lotStock > 0 && remainingStock > 0) { // Generate realistic lot numbers with dates const lotDate = new Date() lotDate.setDate(lotDate.getDate() - Math.floor(Math.random() * 180)) // Random date within last 6 months const formattedDate = lotDate.toISOString().slice(0, 10).replace(/-/g, '') const batchNumber = String(index + 1).padStart(2, '0') stockLocations.push({ id: `stock_${warehouse.id}_${zone.id}_${location.id}_${index}`, warehouseId: warehouse.id, warehouseName: warehouse.name, warehouseCode: warehouse.code, zoneId: zone.id, zoneName: zone.name, zoneCode: zone.zoneCode, locationId: location.id, locationName: location.name, locationCode: location.locationCode, lotNumber: `LOT${formattedDate}-${batchNumber}`, availableQuantity: Math.min(lotStock, remainingStock), requestedQuantity: 0, unitPrice: material.costPrice || 0, }) remainingStock -= Math.min(lotStock, remainingStock) } }) } }) }) }) // Sort by lot number (newest first - FIFO principle) return stockLocations.sort((a, b) => b.lotNumber.localeCompare(a.lotNumber)) } const confirmStockSelection = () => { const selectedLocations = availableStockLocations.filter((loc) => loc.selected) if (selectedLocations.length === 0 || !selectedMaterialForStock) return // Seçilen lokasyonlar için requestedQuantity'i availableQuantity'ye eşitle const locationsWithQuantity = selectedLocations.map((loc) => ({ ...loc, requestedQuantity: loc.availableQuantity, })) // Check if material already exists in the list const existingMaterial = materialsList.find((m) => m.materialId === selectedMaterialForStock) if (existingMaterial) { // Update existing material setMaterialsList((prev) => prev.map((material) => material.id === existingMaterial.id ? { ...material, selectedStockLocations: locationsWithQuantity } : material, ), ) } else { // Add new material const newMaterial: MaterialRow = { id: Date.now().toString(), materialId: selectedMaterialForStock, selectedStockLocations: locationsWithQuantity, } setMaterialsList((prev) => [...prev, newMaterial]) } setShowStockModal(false) setSelectedMaterialForStock('') setAvailableStockLocations([]) setLotSearchTerm('') } const handleSubmit = () => { setShowIssueForm(false) resetForm() } const handleEdit = (movementId: string) => { // Find the movement to edit const movement = mockStockMovements.find((m) => m.id === movementId) if (!movement) return // Load movement data into form setHeaderData({ movementNumber: movement.movementNumber || '', referenceDocument: movement.referenceDocument || '', referenceType: movement.referenceType || 'Sales Order', reason: movement.reason || '', movementDate: movement.movementDate.toISOString().split('T')[0], performedBy: movement.performedBy || '', approvedBy: movement.approvedBy || '', description: movement.description || '', status: movement.status || MovementStatusEnum.Planned, }) // Create material row from movement data if (movement.material && movement.materialId) { // For goods issue, prioritize from fields (since we're issuing FROM the warehouse) const locationId = movement.toLocationId || '' const warehouse = movement.toWarehouse const zone = movement.toZone const location = movement.toLocation const materialRow: MaterialRow = { id: Date.now().toString(), materialId: movement.materialId, selectedStockLocations: [ { id: Date.now().toString(), warehouseId: warehouse?.id || '', warehouseName: warehouse?.name || '', warehouseCode: warehouse?.code || '', zoneId: zone?.id || '', zoneName: zone?.name || '', zoneCode: zone?.zoneCode || '', locationId: locationId, locationName: location?.name || '', locationCode: location?.locationCode || '', lotNumber: movement.lotNumber || '', availableQuantity: movement.quantity || 0, requestedQuantity: movement.quantity || 0, unitPrice: 45.0, selected: true, }, ], } setMaterialsList([materialRow]) } setEditingMovement(movementId) setShowIssueForm(true) } const getStatusLabel = (status: MovementStatusEnum) => { const labels = { [MovementStatusEnum.Planned]: 'Planlandı', [MovementStatusEnum.InProgress]: 'Devam Ediyor', [MovementStatusEnum.Completed]: 'Tamamlandı', [MovementStatusEnum.Cancelled]: 'İptal Edildi', } return labels[status] } const getStatusColor = (status: MovementStatusEnum) => { const colors = { [MovementStatusEnum.Planned]: 'bg-gray-100 text-gray-800', [MovementStatusEnum.InProgress]: 'bg-blue-100 text-blue-800', [MovementStatusEnum.Completed]: 'bg-green-100 text-green-800', [MovementStatusEnum.Cancelled]: 'bg-red-100 text-red-800', } return colors[status] } const getStatusIcon = (status: MovementStatusEnum) => { const icons = { [MovementStatusEnum.Planned]: FaClock, [MovementStatusEnum.InProgress]: FaArrowCircleUp, [MovementStatusEnum.Completed]: FaCheckCircle, [MovementStatusEnum.Cancelled]: FaExclamationCircle, } return icons[status] } // Filter materials that have stock in warehouses (for goods issue) const availableMaterials = mockMaterials.filter( (material) => material.totalStock && material.totalStock > 0, ) const filteredMovements = mockStockMovements.filter((movement) => { const material = movement.material const matchesSearch = movement.movementNumber.toLowerCase().includes(searchTerm.toLowerCase()) || material?.name?.toLowerCase().includes(searchTerm.toLowerCase()) || material?.code?.toLowerCase().includes(searchTerm.toLowerCase()) const matchesStatus = statusFilter === '' || movement.status === statusFilter const matchesType = movement.movementType === MovementTypeEnum.GoodsIssue return matchesSearch && matchesStatus && matchesType }) const MovementDetailModal = () => { const movement = mockStockMovements.find((m) => m.id === selectedMovement) if (!selectedMovement || !movement) return null // Aynı hareket numarasına sahip tüm malzemeleri bul const relatedMovements = mockStockMovements.filter( (m) => m.movementNumber === movement.movementNumber, ) const StatusIcon = getStatusIcon(movement.status!) return (
setSelectedMovement('')} />
{/* Modal Header */}

{movement.movementNumber} - Çıkış Detayları

{relatedMovements.length} malzeme •{' '} {movement.movementDate.toLocaleDateString('tr-TR')}

{/* Modal Content */}
{/* Movement Summary */}

Hareket Özeti

{getStatusLabel(movement.status!)}
Hareket No
{movement.movementNumber}
Tarih
{movement.movementDate.toLocaleDateString('tr-TR')}
Referans
{movement.referenceDocument || '-'}
İşlem Yapan
{movement.performedBy || '-'}
{movement.description && (
Açıklama
{movement.description}
)}
{/* Materials List */}

Çıkış Yapılan Malzemeler ({relatedMovements.length})

{relatedMovements.map((mov, index) => { const warehouse = mockWarehouses.find((w) => w.id === mov.toWarehouseId) const zone = mockWarehouses .flatMap((w) => w.zones || []) .find((z) => z.id === mov.toZoneId) const location = mockWarehouses .flatMap((w) => w.locations || []) .find((l) => l.id === mov.toLocationId) return (
{/* Material Header */}
{index + 1}
{mov.material?.code} - {mov.material?.name}

{mov.material?.materialType?.name}

{mov.quantity} {mov.material?.baseUnit?.name || 'Adet'}
Birim Fiyat: ₺{mov.material?.costPrice?.toFixed(2)}
{/* Material Details */}
{/* Warehouse Info */}
Depo Bilgileri
Depo: {warehouse?.name}
Bölge: {zone?.name || '-'}
Lokasyon:{' '} {location?.name || '-'}
{/* Stock Info */}
Stok Bilgileri
Lot No:{' '} {mov.lotNumber || '-'}
Miktar: {mov.quantity}{' '} {mov.material?.baseUnit?.name || 'Adet'}
Toplam Değer: ₺ {((mov.material?.costPrice || 0) * mov.quantity).toFixed(2)}
{/* Additional Info */}
Ek Bilgiler
Malzeme Grubu:{' '} {mov.material?.materialGroup?.name || '-'}
Referans Tipi:{' '} {mov.referenceType || '-'}
Sebep: {mov.reason || '-'}
) })}
{/* Modal Footer */}
{relatedMovements.length} malzeme
Toplam: ₺ {relatedMovements .reduce( (sum, mov) => sum + (mov.material?.costPrice || 0) * mov.quantity, 0, ) .toFixed(2)}
) } // Stock Selection Modal const StockSelectionModal = () => { if (!showStockModal) return null const selectedMaterial = availableMaterials.find((m) => m.id === selectedMaterialForStock) return createPortal(
setShowStockModal(false)} style={{ zIndex: 99999 }} />
e.stopPropagation()} >

Malzeme ve Stok Lokasyonu Seçimi

💡 Önce malzeme seçin, sonra birden fazla lot ve lokasyondan çıkış yapabilirsiniz.

{/* Material Selection Section */}
{selectedMaterial && selectedMaterialForStock && (
Toplam Stok: {selectedMaterial.totalStock} {selectedMaterial.baseUnit?.code}
Birim: {selectedMaterial.baseUnit?.code}
)}
{/* Lot Search */} {selectedMaterialForStock && availableStockLocations.length > 0 && ( )} {/* Stock Locations Section */} {selectedMaterialForStock && availableStockLocations.length > 0 && ( <>

Stok Lokasyonları {lotSearchTerm && ( ({filteredLocations.length} / {availableStockLocations.length} kayıt) )}

{filteredLocations.map((location, index) => ( ))}
Seç Depo Zone/Lokasyon Lot Numarası Mevcut Stok Birim Fiyat
{ const updatedLocations = availableStockLocations.map((loc) => loc.id === location.id ? { ...loc, selected: e.target.checked, requestedQuantity: e.target.checked ? loc.availableQuantity : 0, } : loc, ) setAvailableStockLocations(updatedLocations) }} className="w-4 h-4 text-red-600 bg-gray-100 border-gray-300 rounded focus:ring-red-500 focus:ring-2" />
{location.warehouseCode}
{location.warehouseName}
{location.zoneCode} / {location.locationCode}
{location.zoneName} / {location.locationName}
{location.lotNumber}
{location.availableQuantity} {selectedMaterial?.baseUnit?.code}
₺{location.unitPrice.toFixed(2)}
Seçili Lokasyon Sayısı:{' '} {availableStockLocations.filter((loc) => loc.selected).length} adet
Toplam Stok Miktarı:{' '} {availableStockLocations .filter((loc) => loc.selected) .reduce((sum, loc) => sum + loc.availableQuantity, 0)}{' '} {selectedMaterial?.baseUnit?.code}
💡 Çıkış miktarlarını ana ekranda düzenleyebilirsiniz
)} {/* No material selected message */} {!selectedMaterialForStock && (

Önce bir malzeme seçiniz

)}
, document.body, ) } return (

Stok Çıkış

Depodan malzeme çıkış hareketlerini yönetin

{/* Stats Cards */}
m.movementDate.toDateString() === new Date().toDateString() && m.status === MovementStatusEnum.Completed && m.movementType === MovementTypeEnum.GoodsIssue, ).length } color="red" icon="FaArrowCircleUp" /> m.status === MovementStatusEnum.Planned && m.movementType === MovementTypeEnum.GoodsIssue, ).length } color="yellow" icon="FaClock" /> m.status === MovementStatusEnum.InProgress && m.movementType === MovementTypeEnum.GoodsIssue, ).length } color="blue" icon="FaBox" /> m.status === MovementStatusEnum.Completed && m.movementType === MovementTypeEnum.GoodsIssue, ).length } color="green" icon="FaCheckCircle" />
{/* Filters */}
setSearchTerm(e.target.value)} className="pl-10 pr-4 py-1.5 text-sm w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent" />
{/* Movements Table */}
{filteredMovements.map((movement) => { const StatusIcon = getStatusIcon(movement.status!) return ( ) })}
Hareket No Malzeme Depo / Bölge / Lokasyon Miktar / Lot No Referans Tarih Durum İşlemler
{movement.movementNumber}
{movement.performedBy}
{movement.material?.name}
{movement.material?.code}
{movement.toWarehouse?.name || movement.toWarehouse?.code}
{movement.toZone?.name && `${movement.toZone.name}`} {movement.toLocation?.name && ` / ${movement.toLocation.name}`}
{movement.toLocation?.locationCode && (
Kod: {movement.toLocation.locationCode}
)}
{movement.quantity} {movement.unit?.code}
{movement.lotNumber && (
Lot: {movement.lotNumber}
)}
{movement.referenceDocument}
{movement.referenceType}
{movement.movementDate.toLocaleDateString('tr-TR')} {getStatusLabel(movement.status!)}
{/* Movement Detail Modal */} {/* Stock Selection Modal */} {/* Issue Form Modal */} {showIssueForm && (
{ setShowIssueForm(false) resetForm() }} />

{editingMovement ? 'Stok Çıkış Düzenle' : 'Yeni Stok Çıkış'}

{/* Header Section - Compact and Responsive */}

Genel Bilgiler

{/* Movement Number */}
handleHeaderInputChange('movementNumber', e.target.value)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-red-500 focus:border-transparent" placeholder="Otomatik" disabled={!!editingMovement} />
{/* Movement Date */}
handleHeaderInputChange('movementDate', e.target.value)} className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-red-500 focus:border-transparent" required />
{/* Reference Document */}
handleHeaderInputChange('referenceDocument', e.target.value) } className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-transparent" placeholder="Belge no" />
{/* Reference Type */}
{/* Performed By */}
{/* Status */}
{/* Description - Full width */}