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

1508 lines
69 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, 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'
import { getMovementStatusColor, getMovementStatusIcon, getMovementStatusText } from '@/utils/erp'
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<HTMLInputElement>(null)
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const inputValue = (e.target as HTMLInputElement).value
onSearch(inputValue)
}
}
const handleClearSearch = () => {
if (inputRef.current) {
inputRef.current.value = ''
onSearch('')
}
}
return (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
Lot Numarası Arama (Enter ile arayın)
</label>
<div className="relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
ref={inputRef}
type="text"
placeholder="Lot numarası yazıp Enter'a basın..."
defaultValue={value}
onKeyDown={handleKeyDown}
className="pl-10 pr-10 py-2 w-full text-sm border border-gray-300 rounded focus:ring-1 focus:ring-red-500 focus:border-transparent"
/>
{value && (
<button
onClick={handleClearSearch}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
title="Aramayı temizle"
>
<FaTimes className="w-4 h-4" />
</button>
)}
</div>
</div>
)
},
)
const WarehouseIssue: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState<MovementStatusEnum | ''>('')
const [selectedMovement, setSelectedMovement] = useState<string>('')
const [showIssueForm, setShowIssueForm] = useState(false)
const [editingMovement, setEditingMovement] = useState<string | null>(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<MaterialRow[]>([])
// Stock location modal state
const [showStockModal, setShowStockModal] = useState(false)
const [selectedMaterialForStock, setSelectedMaterialForStock] = useState<string>('')
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)
}
// 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 = getMovementStatusIcon(movement.status!)
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
onClick={() => setSelectedMovement('')}
/>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle w-full max-w-7xl mx-4 sm:mx-auto">
<div className="bg-white">
{/* Modal Header */}
<div className="bg-gradient-to-r from-red-600 to-red-700 px-4 sm:px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="p-2 bg-white bg-opacity-20 rounded-lg">
<FaArrowCircleUp className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="text-lg sm:text-xl font-semibold text-white">
{movement.movementNumber} - Çıkış Detayları
</h3>
<p className="text-red-100 text-sm">
{relatedMovements.length} malzeme {' '}
{movement.movementDate.toLocaleDateString('tr-TR')}
</p>
</div>
</div>
<button
onClick={() => setSelectedMovement('')}
className="text-white hover:text-red-100 transition-colors p-2 hover:bg-white hover:bg-opacity-20 rounded-lg"
>
<FaTimes className="w-6 h-6" />
</button>
</div>
</div>
{/* Modal Content */}
<div className="px-4 sm:px-6 py-6 max-h-[70vh] overflow-y-auto">
{/* Movement Summary */}
<div className="bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl p-3 sm:p-3 mb-2 border border-gray-200">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-semibold text-gray-900 flex items-center">
<FaInfoCircle className="w-5 h-5 text-gray-600 mr-2" />
Hareket Özeti
</h4>
<div
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getMovementStatusColor(
movement.status!,
)}`}
>
<StatusIcon className="w-4 h-4 mr-1" />
{getMovementStatusText(movement.status!)}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div className="bg-white rounded-lg p-3 border border-gray-200">
<div className="text-gray-600 font-medium mb-1">Hareket No</div>
<div className="text-gray-900 font-semibold">{movement.movementNumber}</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
<div className="text-gray-600 font-medium mb-1">Tarih</div>
<div className="text-gray-900 font-semibold">
{movement.movementDate.toLocaleDateString('tr-TR')}
</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
<div className="text-gray-600 font-medium mb-1">Referans</div>
<div className="text-gray-900 font-semibold">
{movement.referenceDocument || '-'}
</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
<div className="text-gray-600 font-medium mb-1">İşlem Yapan</div>
<div className="text-gray-900 font-semibold">
{movement.performedBy || '-'}
</div>
</div>
</div>
{movement.description && (
<div className="mt-4 bg-white rounded-lg p-3 border border-gray-200">
<div className="text-gray-600 font-medium mb-1">ıklama</div>
<div className="text-gray-900">{movement.description}</div>
</div>
)}
</div>
{/* Materials List */}
<div className="space-y-4">
<h4 className="text-lg font-semibold text-gray-900 flex items-center">
<FaBox className="w-5 h-5 text-gray-600 mr-2" />
Çıkış Yapılan Malzemeler ({relatedMovements.length})
</h4>
<div className="grid gap-4">
{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 (
<div
key={mov.id}
className="bg-white border border-gray-200 rounded-xl overflow-hidden hover:shadow-lg transition-shadow"
>
{/* Material Header */}
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center">
<span className="text-red-600 font-semibold text-sm">
{index + 1}
</span>
</div>
<div>
<h5 className="font-semibold text-gray-900">
{mov.material?.code} - {mov.material?.name}
</h5>
<p className="text-sm text-gray-600">
{mov.material?.materialType?.name}
</p>
</div>
</div>
<div className="text-right">
<div className="text-lg font-bold text-red-600">
{mov.quantity} {mov.material?.baseUnit?.name || 'Adet'}
</div>
<div className="text-sm text-gray-600">
Birim Fiyat: {mov.material?.costPrice?.toFixed(2)}
</div>
</div>
</div>
</div>
{/* Material Details */}
<div className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Warehouse Info */}
<div className="space-y-2">
<h6 className="font-medium text-gray-700 flex items-center">
<FaWarehouse className="w-4 h-4 mr-2 text-gray-500" />
Depo Bilgileri
</h6>
<div className="bg-gray-50 rounded-lg p-3 space-y-1 text-sm">
<div>
<span className="font-medium">Depo:</span> {warehouse?.name}
</div>
<div>
<span className="font-medium">Bölge:</span> {zone?.name || '-'}
</div>
<div>
<span className="font-medium">Lokasyon:</span>{' '}
{location?.name || '-'}
</div>
</div>
</div>
{/* Stock Info */}
<div className="space-y-2">
<h6 className="font-medium text-gray-700 flex items-center">
<FaBarcode className="w-4 h-4 mr-2 text-gray-500" />
Stok Bilgileri
</h6>
<div className="bg-gray-50 rounded-lg p-3 space-y-1 text-sm">
<div>
<span className="font-medium">Lot No:</span>{' '}
{mov.lotNumber || '-'}
</div>
<div>
<span className="font-medium">Miktar:</span> {mov.quantity}{' '}
{mov.material?.baseUnit?.name || 'Adet'}
</div>
<div>
<span className="font-medium">Toplam Değer:</span>
{((mov.material?.costPrice || 0) * mov.quantity).toFixed(2)}
</div>
</div>
</div>
{/* Additional Info */}
<div className="space-y-2">
<h6 className="font-medium text-gray-700 flex items-center">
<FaInfoCircle className="w-4 h-4 mr-2 text-gray-500" />
Ek Bilgiler
</h6>
<div className="bg-gray-50 rounded-lg p-3 space-y-1 text-sm">
<div>
<span className="font-medium">Malzeme Grubu:</span>{' '}
{mov.material?.materialGroup?.name || '-'}
</div>
<div>
<span className="font-medium">Referans Tipi:</span>{' '}
{mov.referenceType || '-'}
</div>
<div>
<span className="font-medium">Sebep:</span> {mov.reason || '-'}
</div>
</div>
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
{/* Modal Footer */}
<div className="bg-gray-50 px-4 sm:px-6 py-4 border-t border-gray-200">
<div className="flex flex-col sm:flex-row justify-between items-center space-y-3 sm:space-y-0">
<div className="flex items-center space-x-4 text-sm text-gray-600">
<div className="flex items-center">
<FaBox className="w-4 h-4 mr-1" />
<span>{relatedMovements.length} malzeme</span>
</div>
<div className="flex items-center">
<FaCalculator className="w-4 h-4 mr-1" />
<span>
Toplam:
{relatedMovements
.reduce(
(sum, mov) => sum + (mov.material?.costPrice || 0) * mov.quantity,
0,
)
.toFixed(2)}
</span>
</div>
</div>
<button
onClick={() => setSelectedMovement('')}
className="bg-gray-600 text-white px-6 py-2 rounded-lg hover:bg-gray-700 transition-colors flex items-center space-x-2"
>
<span>Kapat</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
// Stock Selection Modal
const StockSelectionModal = () => {
if (!showStockModal) return null
const selectedMaterial = availableMaterials.find((m) => m.id === selectedMaterialForStock)
return createPortal(
<div className="fixed inset-0 overflow-y-auto" style={{ zIndex: 99999 }}>
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-900 bg-opacity-75 transition-opacity"
onClick={() => setShowStockModal(false)}
style={{ zIndex: 99999 }}
/>
<div
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle w-full max-w-6xl mx-4 sm:mx-auto relative"
style={{ zIndex: 100000 }}
onClick={(e) => e.stopPropagation()}
>
<div className="bg-white px-4 sm:px-6 pt-5 pb-4">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-medium text-gray-900">
Malzeme ve Stok Lokasyonu Seçimi
</h3>
<p className="text-xs text-blue-600 mt-1">
💡 Önce malzeme seçin, sonra birden fazla lot ve lokasyondan çıkış
yapabilirsiniz.
</p>
</div>
<button
onClick={() => setShowStockModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-6 h-6" />
</button>
</div>
{/* Material Selection Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-3">
<div>
<select
value={selectedMaterialForStock || ''}
onChange={(e) => {
const newValue = e.target.value
if (newValue !== selectedMaterialForStock) {
setSelectedMaterialForStock(newValue)
// Reset stock locations when material changes
setAvailableStockLocations([])
if (newValue) {
const stockLocations = generateStockLocationsForMaterial(newValue)
setAvailableStockLocations(stockLocations)
}
}
}}
className="w-full px-3 py-2 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-red-500 focus:border-transparent"
required
>
<option value="">Malzeme Seçiniz</option>
{availableMaterials.map((mat) => (
<option key={mat.id} value={mat.id}>
{mat.code} - {mat.name}
</option>
))}
</select>
</div>
{selectedMaterial && selectedMaterialForStock && (
<div className="flex items-center space-x-4 text-sm">
<div>
<span className="text-gray-600">Toplam Stok:</span>
<span className="ml-1 font-medium text-green-600">
{selectedMaterial.totalStock} {selectedMaterial.baseUnit?.code}
</span>
</div>
<div>
<span className="text-gray-600">Birim:</span>
<span className="ml-1 font-medium">{selectedMaterial.baseUnit?.code}</span>
</div>
</div>
)}
</div>
{/* Lot Search */}
{selectedMaterialForStock && availableStockLocations.length > 0 && (
<LotSearchInput value={lotSearchTerm} onSearch={setLotSearchTerm} />
)}
{/* Stock Locations Section */}
{selectedMaterialForStock && availableStockLocations.length > 0 && (
<>
<div className="flex items-center justify-between m-1">
<h4 className="text-sm font-medium text-gray-900">
Stok Lokasyonları
{lotSearchTerm && (
<span className="ml-2 text-xs text-blue-600">
({filteredLocations.length} / {availableStockLocations.length} kayıt)
</span>
)}
</h4>
</div>
<div className="max-h-96 overflow-y-auto border border-gray-200 rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
Seç
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Depo
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Zone/Lokasyon
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Lot Numarası
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Mevcut Stok
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Birim Fiyat
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredLocations.map((location, index) => (
<tr
key={`stock-row-${location.id}-${location.lotNumber}`}
className={`hover:bg-gray-50 ${
index % 2 === 0 ? 'bg-white' : 'bg-gray-25'
}`}
>
<td className="px-3 py-3 whitespace-nowrap text-center">
<input
type="checkbox"
checked={location.selected || false}
onChange={(e) => {
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"
/>
</td>
<td className="px-3 py-3 whitespace-nowrap text-sm">
<div className="flex items-center">
<FaBox className="w-4 h-4 text-blue-500 mr-2" />
<div>
<div className="font-medium text-gray-900">
{location.warehouseCode}
</div>
<div className="text-gray-500 text-xs">
{location.warehouseName}
</div>
</div>
</div>
</td>
<td className="px-3 py-3 whitespace-nowrap text-sm">
<div className="flex items-center">
<FaMapMarkerAlt className="w-3 h-3 text-green-500 mr-2" />
<div>
<div className="font-medium text-gray-900">
{location.zoneCode} / {location.locationCode}
</div>
<div className="text-gray-500 text-xs">
{location.zoneName} / {location.locationName}
</div>
</div>
</div>
</td>
<td className="px-3 py-3 whitespace-nowrap text-sm">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{location.lotNumber}
</span>
</td>
<td className="px-3 py-3 whitespace-nowrap text-sm">
<div className="font-medium text-gray-900">
{location.availableQuantity} {selectedMaterial?.baseUnit?.code}
</div>
</td>
<td className="px-3 py-3 whitespace-nowrap text-sm text-gray-900">
{location.unitPrice.toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-between items-center pt-4 border-t border-gray-200 mt-4">
<div className="text-sm space-y-1">
<div className="text-gray-600">
<strong>Seçili Lokasyon Sayısı:</strong>{' '}
<span className="text-blue-600 font-medium">
{availableStockLocations.filter((loc) => loc.selected).length} adet
</span>
</div>
<div className="text-gray-600">
<strong>Toplam Stok Miktarı:</strong>{' '}
<span className="text-green-600 font-medium">
{availableStockLocations
.filter((loc) => loc.selected)
.reduce((sum, loc) => sum + loc.availableQuantity, 0)}{' '}
{selectedMaterial?.baseUnit?.code}
</span>
</div>
<div className="text-xs text-blue-600">
💡 Çıkış miktarlarını ana ekranda düzenleyebilirsiniz
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowStockModal(false)}
className="px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
İptal
</button>
<button
onClick={confirmStockSelection}
className="px-4 py-2 text-sm bg-red-600 text-white hover:bg-red-700 rounded-lg transition-colors flex items-center gap-2"
disabled={
!selectedMaterialForStock ||
!availableStockLocations.some((loc) => loc.selected)
}
>
<FaCheckCircle className="w-4 h-4" />
Seçimi Onayla
</button>
</div>
</div>
</>
)}
{/* No material selected message */}
{!selectedMaterialForStock && (
<div className="text-center py-8 text-gray-500">
<FaBox className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>Önce bir malzeme seçiniz</p>
</div>
)}
</div>
</div>
</div>
</div>,
document.body,
)
}
return (
<Container>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Stok Çıkış</h2>
<p className="text-gray-600">Depodan malzeme çıkış hareketlerini yönetin</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowIssueForm(true)}
className="bg-red-600 text-white px-4 py-1.5 text-sm rounded-lg hover:bg-red-700 flex items-center gap-2"
>
<FaPlus className="w-4 h-4" />
Yeni Çıkış
</button>
<button className="bg-green-600 text-white px-4 py-1.5 text-sm rounded-lg hover:bg-green-700 flex items-center gap-2">
<FaUpload className="w-4 h-4" />
İçe Aktar
</button>
<button className="bg-gray-600 text-white px-4 py-1.5 text-sm rounded-lg hover:bg-gray-700 flex items-center gap-2">
<FaDownload className="w-4 h-4" />
Dışa Aktar
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget
title="Bugün Çıkış"
value={
mockStockMovements.filter(
(m) =>
m.movementDate.toDateString() === new Date().toDateString() &&
m.status === MovementStatusEnum.Completed &&
m.movementType === MovementTypeEnum.GoodsIssue,
).length
}
color="red"
icon="FaArrowCircleUp"
/>
<Widget
title="Bekleyen"
value={
mockStockMovements.filter(
(m) =>
m.status === MovementStatusEnum.Planned &&
m.movementType === MovementTypeEnum.GoodsIssue,
).length
}
color="yellow"
icon="FaClock"
/>
<Widget
title="İşlemde"
value={
mockStockMovements.filter(
(m) =>
m.status === MovementStatusEnum.InProgress &&
m.movementType === MovementTypeEnum.GoodsIssue,
).length
}
color="blue"
icon="FaBox"
/>
<Widget
title="Tamamlanan"
value={
mockStockMovements.filter(
(m) =>
m.status === MovementStatusEnum.Completed &&
m.movementType === MovementTypeEnum.GoodsIssue,
).length
}
color="green"
icon="FaCheckCircle"
/>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder=ıkış ara..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as MovementStatusEnum | '')}
className="px-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
>
<option value="">Tüm Durumlar</option>
{Object.values(MovementStatusEnum).map((status) => (
<option key={status} value={status}>
{getMovementStatusText(status)}
</option>
))}
</select>
</div>
{/* Movements Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Hareket No
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Malzeme
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Depo / Bölge / Lokasyon
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Miktar / Lot No
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Referans
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tarih
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Durum
</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
İşlemler
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredMovements.map((movement) => {
const StatusIcon = getMovementStatusIcon(movement.status!)
return (
<tr key={movement.id} className="hover:bg-gray-50">
<td className="px-3 py-2 whitespace-nowrap">
<div className="flex items-center">
<div className="p-2 bg-red-100 rounded-lg mr-3">
<FaArrowCircleUp className="w-4 h-4 text-red-600" />
</div>
<div>
<div className="text-sm font-medium text-gray-900">
{movement.movementNumber}
</div>
<div className="text-sm text-gray-500">{movement.performedBy}</div>
</div>
</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{movement.material?.name}
</div>
<div className="text-sm text-gray-500">{movement.material?.code}</div>
</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{movement.toWarehouse?.name || movement.toWarehouse?.code}
</div>
<div className="text-sm text-gray-500">
{movement.toZone?.name && `${movement.toZone.name}`}
{movement.toLocation?.name && ` / ${movement.toLocation.name}`}
</div>
{movement.toLocation?.locationCode && (
<div className="text-xs text-gray-400">
Kod: {movement.toLocation.locationCode}
</div>
)}
</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="text-sm text-gray-900">
{movement.quantity} {movement.unit?.code}
</div>
{movement.lotNumber && (
<div className="text-sm text-gray-500">Lot: {movement.lotNumber}</div>
)}
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="text-sm text-gray-900">{movement.referenceDocument}</div>
<div className="text-sm text-gray-500">{movement.referenceType}</div>
</td>
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
{movement.movementDate.toLocaleDateString('tr-TR')}
</td>
<td className="px-3 py-2 whitespace-nowrap">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMovementStatusColor(
movement.status!,
)}`}
>
<StatusIcon className="w-3 h-3 mr-1" />
{getMovementStatusText(movement.status!)}
</span>
</td>
<td className="px-3 py-2 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setSelectedMovement(movement.id)}
className="text-red-600 hover:text-red-900"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(movement.id)}
className="text-red-600 hover:text-red-900"
>
<FaEdit className="w-4 h-4" />
</button>
<button className="text-red-600 hover:text-red-900">
<FaTrash className="w-4 h-4" />
</button>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
{/* Movement Detail Modal */}
<MovementDetailModal />
{/* Stock Selection Modal */}
<StockSelectionModal />
{/* Issue Form Modal */}
{showIssueForm && (
<div className="fixed inset-0 overflow-y-auto" style={{ zIndex: 50 }}>
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
onClick={() => {
setShowIssueForm(false)
resetForm()
}}
/>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle w-full max-w-7xl mx-4 sm:mx-auto">
<div className="bg-white px-4 sm:px-6 pt-5 pb-4 sm:p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-medium text-gray-900">
{editingMovement ? 'Stok Çıkış Düzenle' : 'Yeni Stok Çıkış'}
</h3>
<button
onClick={() => {
setShowIssueForm(false)
resetForm()
}}
className="text-gray-400 hover:text-gray-600"
>
<FaTimes className="w-6 h-6" />
</button>
</div>
{/* Header Section - Compact and Responsive */}
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<h4 className="text-sm font-medium text-gray-900 mb-3">Genel Bilgiler</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 text-sm">
{/* Movement Number */}
<div className="sm:col-span-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
Hareket No
</label>
<input
type="text"
value={headerData.movementNumber}
onChange={(e) => 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}
/>
</div>
{/* Movement Date */}
<div className="sm:col-span-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
Tarih *
</label>
<input
type="date"
value={headerData.movementDate}
onChange={(e) => 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
/>
</div>
{/* Reference Document */}
<div className="sm:col-span-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
Referans Belge
</label>
<input
type="text"
value={headerData.referenceDocument}
onChange={(e) =>
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"
/>
</div>
{/* Reference Type */}
<div className="sm:col-span-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
Referans Tipi
</label>
<select
value={headerData.referenceType}
onChange={(e) => handleHeaderInputChange('referenceType', 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"
>
<option value="Sales Order">Satış Siparişi</option>
<option value="Production Order">Üretim Emri</option>
<option value="Transfer Order">Transfer Emri</option>
<option value="Return">İade</option>
<option value="Adjustment">Düzeltme</option>
<option value="Other">Diğer</option>
</select>
</div>
{/* Performed By */}
<div className="sm:col-span-1">
<label className="block text-xs font-medium text-gray-700 mb-1">
İşlem Yapan *
</label>
<select
value={headerData.performedBy}
onChange={(e) => handleHeaderInputChange('performedBy', 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
>
<option value="">Çalışan Seçiniz</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.fullName}>
{employee.fullName} - {employee.code}
</option>
))}
</select>
</div>
{/* Status */}
<div className="sm:col-span-1">
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
<select
value={headerData.status}
onChange={(e) =>
handleHeaderInputChange('status', e.target.value as MovementStatusEnum)
}
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"
>
{Object.values(MovementStatusEnum).map((status) => (
<option key={status} value={status}>
{getMovementStatusText(status)}
</option>
))}
</select>
</div>
{/* Description - Full width */}
<div className="col-span-1 sm:col-span-2 lg:col-span-3 xl:col-span-6">
<label className="block text-xs font-medium text-gray-700 mb-1">
ıklama
</label>
<textarea
value={headerData.description}
onChange={(e) => handleHeaderInputChange('description', 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="Hareket açıklaması"
rows={2}
/>
</div>
</div>
</div>
{/* Materials Section - Improved Design */}
<div className="m-2">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-900">
Çıkış Malzemeleri
{materialsList.length > 0 && (
<span className="ml-2 text-xs text-blue-600">
({materialsList.length} malzeme)
</span>
)}
</h4>
</div>
{/* Material List Container */}
<div className="border border-gray-200 rounded-lg bg-gray-50 min-h-[200px] max-h-[400px] overflow-y-auto">
{materialsList.length === 0 ? (
<div className="flex flex-col items-center justify-center h-[200px] text-gray-500">
<FaBox className="w-12 h-12 mb-3 opacity-50" />
<p className="text-sm font-medium">Henüz malzeme seçilmedi</p>
<p className="text-xs text-gray-400 mt-1 text-center">
Aşağıdaki "Malzeme Seç" butonuna tıklayarak
<br />
malzeme ekleyebilirsiniz
</p>
</div>
) : (
<div className="p-3 space-y-3">
{materialsList.map((materialRow, index) => {
const selectedMaterial = availableMaterials.find(
(m) => m.id === materialRow.materialId,
)
const totalQuantity = materialRow.selectedStockLocations.reduce(
(sum, loc) => sum + loc.requestedQuantity,
0,
)
return (
<div
key={materialRow.id}
className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow"
>
{/* Material Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center w-8 h-8 bg-red-100 rounded-full">
<span className="text-xs font-bold text-red-600">
{index + 1}
</span>
</div>
<div>
<div className="flex items-center space-x-2">
<span className="text-sm font-semibold text-gray-900">
{selectedMaterial?.code}
</span>
<span className="px-2 py-1 bg-gray-100 text-xs text-gray-600 rounded">
{selectedMaterial?.baseUnit?.code}
</span>
</div>
<p className="text-xs text-gray-600 mt-1">
{selectedMaterial?.name}
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<div className="text-right">
<div className="text-sm font-semibold text-red-600">
{totalQuantity} {selectedMaterial?.baseUnit?.code}
</div>
<div className="text-xs text-gray-500">
{materialRow.selectedStockLocations.length} lot
</div>
</div>
<div className="flex space-x-1">
<button
onClick={() => removeMaterial(materialRow.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Compact stock locations view with quantity inputs */}
{materialRow.selectedStockLocations.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-100">
<div className="space-y-2">
{materialRow.selectedStockLocations.map((location) => (
<div
key={location.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded text-xs border"
>
<div className="flex items-center space-x-3 flex-1">
<span className="font-medium text-gray-900">
{location.warehouseCode} {' - '}
{location.warehouseName}
</span>
<span className="text-gray-500">
{location.zoneCode}-{location.zoneName} {' / '}
{location.locationCode}-{location.locationName}
</span>
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{location.lotNumber}
</span>
<span className="text-gray-500">
Mevcut: {location.availableQuantity}{' '}
{selectedMaterial?.baseUnit?.code}
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-gray-600 text-xs">Çıkış:</span>
<input
type="number"
value={location.requestedQuantity || ''}
onChange={(e) => {
const value = parseFloat(e.target.value) || 0
const updatedMaterials = materialsList.map(
(material) =>
material.id === materialRow.id
? {
...material,
selectedStockLocations:
material.selectedStockLocations.map(
(loc) =>
loc.id === location.id
? {
...loc,
requestedQuantity: Math.min(
value,
loc.availableQuantity,
),
}
: loc,
),
}
: material,
)
setMaterialsList(updatedMaterials)
}}
className="w-20 px-2 py-1 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-red-500"
min="0"
max={location.availableQuantity}
step="0.01"
/>
<span className="text-xs text-gray-500">
{selectedMaterial?.baseUnit?.code}
</span>
</div>
</div>
))}
</div>
<div className="mt-2 text-xs text-gray-600 text-right">
Toplam Çıkış:{' '}
<span className="font-medium text-red-600">
{materialRow.selectedStockLocations.reduce(
(sum, loc) => sum + (loc.requestedQuantity || 0),
0,
)}{' '}
{selectedMaterial?.baseUnit?.code}
</span>
</div>
</div>
)}
</div>
)
})}
</div>
)}
</div>
</div>
{/* Form Actions */}
<div className="flex justify-between items-center gap-3 pt-4 border-t border-gray-200">
{/* Left side - Add Material Button */}
<button
onClick={() => setShowStockModal(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm hover:bg-blue-700 flex items-center gap-2 shadow-sm transition-colors"
>
<FaPlus className="w-4 h-4" />
Malzeme Seç
</button>
{/* Right side - Form Actions */}
<div className="flex gap-3">
<button
onClick={() => {
setShowIssueForm(false)
resetForm()
}}
className="px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
İptal
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 text-sm bg-red-600 text-white hover:bg-red-700 rounded-lg transition-colors flex items-center gap-2"
>
<FaSave className="w-4 h-4" />
{editingMovement ? 'Güncelle' : 'Kaydet'}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</Container>
)
}
export default WarehouseIssue