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"; 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< StockLocation[] >([]); 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 */}