erp-platform/ui/src/views/warehouse/components/WarehouseIssue.tsx

1509 lines
69 KiB
TypeScript
Raw Normal View History

2025-09-15 21:11:40 +00:00
import React, { useState, useRef, useMemo } from 'react'
import { createPortal } from 'react-dom'
import { MovementStatusEnum, MovementTypeEnum } from '../../../types/mm'
2025-09-15 09:31:47 +00:00
import {
FaSearch,
FaPlus,
FaEdit,
FaTrash,
FaBox,
FaCheckCircle,
FaClock,
FaExclamationCircle,
FaDownload,
FaUpload,
FaEye,
FaArrowCircleUp,
FaSave,
FaTimes,
FaMapMarkerAlt,
FaInfoCircle,
FaWarehouse,
FaBarcode,
FaCalculator,
2025-09-15 21:11:40 +00:00
} 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'
2025-09-17 09:46:58 +00:00
import { getMovementStatusColor, getMovementStatusIcon, getMovementStatusText } from '@/utils/erp'
2025-09-15 09:31:47 +00:00
interface StockLocation {
2025-09-15 21:11:40 +00:00
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
2025-09-15 09:31:47 +00:00
}
interface MaterialRow {
2025-09-15 21:11:40 +00:00
id: string
materialId: string
selectedStockLocations: StockLocation[]
2025-09-15 09:31:47 +00:00
}
// Separate component for lot search input to prevent re-renders
const LotSearchInput = React.memo(
2025-09-15 21:11:40 +00:00
({ value, onSearch }: { value: string; onSearch: (value: string) => void }) => {
const inputRef = useRef<HTMLInputElement>(null)
2025-09-15 09:31:47 +00:00
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
2025-09-15 21:11:40 +00:00
if (e.key === 'Enter') {
const inputValue = (e.target as HTMLInputElement).value
onSearch(inputValue)
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
}
2025-09-15 09:31:47 +00:00
const handleClearSearch = () => {
if (inputRef.current) {
2025-09-15 21:11:40 +00:00
inputRef.current.value = ''
onSearch('')
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
}
2025-09-15 09:31:47 +00:00
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>
2025-09-15 21:11:40 +00:00
)
},
)
2025-09-15 09:31:47 +00:00
const WarehouseIssue: React.FC = () => {
2025-09-15 21:11:40 +00:00
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)
2025-09-15 09:31:47 +00:00
// Form state - Header data for the entire movement
const [headerData, setHeaderData] = useState({
2025-09-15 21:11:40 +00:00
movementNumber: '',
referenceDocument: '',
referenceType: 'Sales Order',
reason: '',
movementDate: new Date().toISOString().split('T')[0],
performedBy: '',
approvedBy: '',
description: '',
2025-09-15 09:31:47 +00:00
status: MovementStatusEnum.Planned,
2025-09-15 21:11:40 +00:00
})
2025-09-15 09:31:47 +00:00
// Materials list for the movement - New structure for stock-based selection
2025-09-15 21:11:40 +00:00
const [materialsList, setMaterialsList] = useState<MaterialRow[]>([])
2025-09-15 09:31:47 +00:00
// Stock location modal state
2025-09-15 21:11:40 +00:00
const [showStockModal, setShowStockModal] = useState(false)
const [selectedMaterialForStock, setSelectedMaterialForStock] = useState<string>('')
const [availableStockLocations, setAvailableStockLocations] = useState<StockLocation[]>([])
const [lotSearchTerm, setLotSearchTerm] = useState('')
2025-09-15 09:31:47 +00:00
// Memoized filtered locations to prevent unnecessary re-renders
const filteredLocations = useMemo(() => {
return availableStockLocations.filter(
(location) =>
2025-09-15 21:11:40 +00:00
lotSearchTerm === '' ||
location.lotNumber.toLowerCase().includes(lotSearchTerm.toLowerCase()),
)
}, [availableStockLocations, lotSearchTerm])
2025-09-15 09:31:47 +00:00
const resetForm = () => {
setHeaderData({
2025-09-15 21:11:40 +00:00
movementNumber: '',
referenceDocument: '',
referenceType: 'Sales Order',
reason: '',
movementDate: new Date().toISOString().split('T')[0],
performedBy: '',
approvedBy: '',
description: '',
2025-09-15 09:31:47 +00:00
status: MovementStatusEnum.Planned,
2025-09-15 21:11:40 +00:00
})
setMaterialsList([])
setEditingMovement(null)
}
2025-09-15 09:31:47 +00:00
const handleHeaderInputChange = (field: string, value: string) => {
setHeaderData((prev) => ({
...prev,
[field]: value,
2025-09-15 21:11:40 +00:00
}))
}
2025-09-15 09:31:47 +00:00
const removeMaterial = (materialId: string) => {
2025-09-15 21:11:40 +00:00
if (window.confirm('Bu malzemeyi listeden kaldırmak istediğinizden emin misiniz?')) {
setMaterialsList((prev) => prev.filter((material) => material.id !== materialId))
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
}
2025-09-15 09:31:47 +00:00
// Generate mock stock locations for a given material
2025-09-15 21:11:40 +00:00
const generateStockLocationsForMaterial = (materialId: string): StockLocation[] => {
const material = mockMaterials.find((m) => m.id === materialId)
if (!material || !material.totalStock || material.totalStock <= 0) return []
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
const stockLocations: StockLocation[] = []
let remainingStock = material.totalStock
2025-09-15 09:31:47 +00:00
// Generate some random stock distributions across warehouses/zones/locations
mockWarehouses.forEach((warehouse) => {
2025-09-15 21:11:40 +00:00
if (remainingStock <= 0) return
2025-09-15 09:31:47 +00:00
warehouse.zones?.forEach((zone) => {
2025-09-15 21:11:40 +00:00
if (remainingStock <= 0) return
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
const locationsInZone = warehouse.locations?.filter((loc) => loc.zoneId === zone.id) || []
2025-09-15 09:31:47 +00:00
locationsInZone.forEach((location) => {
2025-09-15 21:11:40 +00:00
if (remainingStock <= 0) return
2025-09-15 09:31:47 +00:00
// 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
2025-09-15 21:11:40 +00:00
remainingStock,
)
2025-09-15 09:31:47 +00:00
if (stockAtLocation > 0) {
// Generate 2-4 different lots per location with different dates
2025-09-15 21:11:40 +00:00
const lotsCount = Math.floor(Math.random() * 3) + 2 // 2-4 lots
2025-09-15 09:31:47 +00:00
// Split stock among lots with realistic distribution
2025-09-15 21:11:40 +00:00
const lotDistribution = []
let tempStock = stockAtLocation
2025-09-15 09:31:47 +00:00
for (let i = 0; i < lotsCount && tempStock > 5; i++) {
if (i === lotsCount - 1) {
// Last lot gets remaining stock
2025-09-15 21:11:40 +00:00
lotDistribution.push(tempStock)
2025-09-15 09:31:47 +00:00
} else {
// Distribute randomly but ensure each lot has at least 5 units
2025-09-15 21:11:40 +00:00
const maxForThisLot = Math.floor(tempStock * 0.6)
const minForThisLot = Math.min(5, tempStock)
2025-09-15 09:31:47 +00:00
const lotStock =
2025-09-15 21:11:40 +00:00
Math.floor(Math.random() * (maxForThisLot - minForThisLot + 1)) + minForThisLot
lotDistribution.push(lotStock)
tempStock -= lotStock
2025-09-15 09:31:47 +00:00
}
}
// Create lot entries with different dates and batch numbers
lotDistribution.forEach((lotStock, index) => {
if (lotStock > 0 && remainingStock > 0) {
// Generate realistic lot numbers with dates
2025-09-15 21:11:40 +00:00
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')
2025-09-15 09:31:47 +00:00
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,
2025-09-15 21:11:40 +00:00
})
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
remainingStock -= Math.min(lotStock, remainingStock)
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
})
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
})
})
})
2025-09-15 09:31:47 +00:00
// Sort by lot number (newest first - FIFO principle)
2025-09-15 21:11:40 +00:00
return stockLocations.sort((a, b) => b.lotNumber.localeCompare(a.lotNumber))
}
2025-09-15 09:31:47 +00:00
const confirmStockSelection = () => {
2025-09-15 21:11:40 +00:00
const selectedLocations = availableStockLocations.filter((loc) => loc.selected)
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
if (selectedLocations.length === 0 || !selectedMaterialForStock) return
2025-09-15 09:31:47 +00:00
// Seçilen lokasyonlar için requestedQuantity'i availableQuantity'ye eşitle
const locationsWithQuantity = selectedLocations.map((loc) => ({
...loc,
requestedQuantity: loc.availableQuantity,
2025-09-15 21:11:40 +00:00
}))
2025-09-15 09:31:47 +00:00
// Check if material already exists in the list
2025-09-15 21:11:40 +00:00
const existingMaterial = materialsList.find((m) => m.materialId === selectedMaterialForStock)
2025-09-15 09:31:47 +00:00
if (existingMaterial) {
// Update existing material
setMaterialsList((prev) =>
prev.map((material) =>
material.id === existingMaterial.id
? { ...material, selectedStockLocations: locationsWithQuantity }
2025-09-15 21:11:40 +00:00
: material,
),
)
2025-09-15 09:31:47 +00:00
} else {
// Add new material
const newMaterial: MaterialRow = {
id: Date.now().toString(),
materialId: selectedMaterialForStock,
selectedStockLocations: locationsWithQuantity,
2025-09-15 21:11:40 +00:00
}
setMaterialsList((prev) => [...prev, newMaterial])
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
setShowStockModal(false)
setSelectedMaterialForStock('')
setAvailableStockLocations([])
setLotSearchTerm('')
}
2025-09-15 09:31:47 +00:00
const handleSubmit = () => {
2025-09-15 21:11:40 +00:00
setShowIssueForm(false)
resetForm()
}
2025-09-15 09:31:47 +00:00
const handleEdit = (movementId: string) => {
// Find the movement to edit
2025-09-15 21:11:40 +00:00
const movement = mockStockMovements.find((m) => m.id === movementId)
if (!movement) return
2025-09-15 09:31:47 +00:00
// Load movement data into form
setHeaderData({
2025-09-15 21:11:40 +00:00
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 || '',
2025-09-15 09:31:47 +00:00
status: movement.status || MovementStatusEnum.Planned,
2025-09-15 21:11:40 +00:00
})
2025-09-15 09:31:47 +00:00
// Create material row from movement data
if (movement.material && movement.materialId) {
// For goods issue, prioritize from fields (since we're issuing FROM the warehouse)
2025-09-15 21:11:40 +00:00
const locationId = movement.toLocationId || ''
const warehouse = movement.toWarehouse
const zone = movement.toZone
const location = movement.toLocation
2025-09-15 09:31:47 +00:00
const materialRow: MaterialRow = {
id: Date.now().toString(),
materialId: movement.materialId,
selectedStockLocations: [
{
id: Date.now().toString(),
2025-09-15 21:11:40 +00:00
warehouseId: warehouse?.id || '',
warehouseName: warehouse?.name || '',
warehouseCode: warehouse?.code || '',
zoneId: zone?.id || '',
zoneName: zone?.name || '',
zoneCode: zone?.zoneCode || '',
2025-09-15 09:31:47 +00:00
locationId: locationId,
2025-09-15 21:11:40 +00:00
locationName: location?.name || '',
locationCode: location?.locationCode || '',
lotNumber: movement.lotNumber || '',
2025-09-15 09:31:47 +00:00
availableQuantity: movement.quantity || 0,
requestedQuantity: movement.quantity || 0,
unitPrice: 45.0,
selected: true,
},
],
2025-09-15 21:11:40 +00:00
}
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
setMaterialsList([materialRow])
2025-09-15 09:31:47 +00:00
}
2025-09-15 21:11:40 +00:00
setEditingMovement(movementId)
setShowIssueForm(true)
}
2025-09-15 09:31:47 +00:00
// Filter materials that have stock in warehouses (for goods issue)
const availableMaterials = mockMaterials.filter(
2025-09-15 21:11:40 +00:00
(material) => material.totalStock && material.totalStock > 0,
)
2025-09-15 09:31:47 +00:00
const filteredMovements = mockStockMovements.filter((movement) => {
2025-09-15 21:11:40 +00:00
const material = movement.material
2025-09-15 09:31:47 +00:00
const matchesSearch =
2025-09-15 21:11:40 +00:00
movement.movementNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
2025-09-15 09:31:47 +00:00
material?.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
2025-09-15 21:11:40 +00:00
material?.code?.toLowerCase().includes(searchTerm.toLowerCase())
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
const matchesStatus = statusFilter === '' || movement.status === statusFilter
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
const matchesType = movement.movementType === MovementTypeEnum.GoodsIssue
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
return matchesSearch && matchesStatus && matchesType
})
2025-09-15 09:31:47 +00:00
const MovementDetailModal = () => {
2025-09-15 21:11:40 +00:00
const movement = mockStockMovements.find((m) => m.id === selectedMovement)
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
if (!selectedMovement || !movement) return null
2025-09-15 09:31:47 +00:00
// Aynı hareket numarasına sahip tüm malzemeleri bul
const relatedMovements = mockStockMovements.filter(
2025-09-15 21:11:40 +00:00
(m) => m.movementNumber === movement.movementNumber,
)
2025-09-15 09:31:47 +00:00
2025-09-17 09:46:58 +00:00
const StatusIcon = getMovementStatusIcon(movement.status!)
2025-09-15 09:31:47 +00:00
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"
2025-09-15 21:11:40 +00:00
onClick={() => setSelectedMovement('')}
2025-09-15 09:31:47 +00:00
/>
<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">
2025-09-15 21:11:40 +00:00
{relatedMovements.length} malzeme {' '}
{movement.movementDate.toLocaleDateString('tr-TR')}
2025-09-15 09:31:47 +00:00
</p>
</div>
</div>
<button
2025-09-15 21:11:40 +00:00
onClick={() => setSelectedMovement('')}
2025-09-15 09:31:47 +00:00
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
2025-09-17 09:46:58 +00:00
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getMovementStatusColor(
2025-09-15 21:11:40 +00:00
movement.status!,
2025-09-15 09:31:47 +00:00
)}`}
>
<StatusIcon className="w-4 h-4 mr-1" />
2025-09-17 09:46:58 +00:00
{getMovementStatusText(movement.status!)}
2025-09-15 09:31:47 +00:00
</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">
2025-09-15 21:11:40 +00:00
<div className="text-gray-600 font-medium mb-1">Hareket No</div>
<div className="text-gray-900 font-semibold">{movement.movementNumber}</div>
2025-09-15 09:31:47 +00:00
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
2025-09-15 21:11:40 +00:00
<div className="text-gray-600 font-medium mb-1">Tarih</div>
2025-09-15 09:31:47 +00:00
<div className="text-gray-900 font-semibold">
2025-09-15 21:11:40 +00:00
{movement.movementDate.toLocaleDateString('tr-TR')}
2025-09-15 09:31:47 +00:00
</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
2025-09-15 21:11:40 +00:00
<div className="text-gray-600 font-medium mb-1">Referans</div>
2025-09-15 09:31:47 +00:00
<div className="text-gray-900 font-semibold">
2025-09-15 21:11:40 +00:00
{movement.referenceDocument || '-'}
2025-09-15 09:31:47 +00:00
</div>
</div>
<div className="bg-white rounded-lg p-3 border border-gray-200">
2025-09-15 21:11:40 +00:00
<div className="text-gray-600 font-medium mb-1">İşlem Yapan</div>
2025-09-15 09:31:47 +00:00
<div className="text-gray-900 font-semibold">
2025-09-15 21:11:40 +00:00
{movement.performedBy || '-'}
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
{movement.description && (
<div className="mt-4 bg-white rounded-lg p-3 border border-gray-200">
2025-09-15 21:11:40 +00:00
<div className="text-gray-600 font-medium mb-1">ıklama</div>
<div className="text-gray-900">{movement.description}</div>
2025-09-15 09:31:47 +00:00
</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) => {
2025-09-15 21:11:40 +00:00
const warehouse = mockWarehouses.find((w) => w.id === mov.toWarehouseId)
2025-09-15 09:31:47 +00:00
const zone = mockWarehouses
.flatMap((w) => w.zones || [])
2025-09-15 21:11:40 +00:00
.find((z) => z.id === mov.toZoneId)
2025-09-15 09:31:47 +00:00
const location = mockWarehouses
.flatMap((w) => w.locations || [])
2025-09-15 21:11:40 +00:00
.find((l) => l.id === mov.toLocationId)
2025-09-15 09:31:47 +00:00
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">
2025-09-15 21:11:40 +00:00
{mov.quantity} {mov.material?.baseUnit?.name || 'Adet'}
2025-09-15 09:31:47 +00:00
</div>
<div className="text-sm text-gray-600">
2025-09-15 21:11:40 +00:00
Birim Fiyat: {mov.material?.costPrice?.toFixed(2)}
2025-09-15 09:31:47 +00:00
</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>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Depo:</span> {warehouse?.name}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Bölge:</span> {zone?.name || '-'}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Lokasyon:</span>{' '}
{location?.name || '-'}
2025-09-15 09:31:47 +00:00
</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>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Lot No:</span>{' '}
{mov.lotNumber || '-'}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Miktar:</span> {mov.quantity}{' '}
{mov.material?.baseUnit?.name || 'Adet'}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Toplam Değer:</span>
{((mov.material?.costPrice || 0) * mov.quantity).toFixed(2)}
2025-09-15 09:31:47 +00:00
</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>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Malzeme Grubu:</span>{' '}
{mov.material?.materialGroup?.name || '-'}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Referans Tipi:</span>{' '}
{mov.referenceType || '-'}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 21:11:40 +00:00
<span className="font-medium">Sebep:</span> {mov.reason || '-'}
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
</div>
</div>
</div>
2025-09-15 21:11:40 +00:00
)
2025-09-15 09:31:47 +00:00
})}
</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(
2025-09-15 21:11:40 +00:00
(sum, mov) => sum + (mov.material?.costPrice || 0) * mov.quantity,
0,
2025-09-15 09:31:47 +00:00
)
.toFixed(2)}
</span>
</div>
</div>
<button
2025-09-15 21:11:40 +00:00
onClick={() => setSelectedMovement('')}
2025-09-15 09:31:47 +00:00
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>
2025-09-15 21:11:40 +00:00
)
}
2025-09-15 09:31:47 +00:00
// Stock Selection Modal
const StockSelectionModal = () => {
2025-09-15 21:11:40 +00:00
if (!showStockModal) return null
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
const selectedMaterial = availableMaterials.find((m) => m.id === selectedMaterialForStock)
2025-09-15 09:31:47 +00:00
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">
2025-09-15 21:11:40 +00:00
💡 Önce malzeme seçin, sonra birden fazla lot ve lokasyondan çıkış
yapabilirsiniz.
2025-09-15 09:31:47 +00:00
</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
2025-09-15 21:11:40 +00:00
value={selectedMaterialForStock || ''}
2025-09-15 09:31:47 +00:00
onChange={(e) => {
2025-09-15 21:11:40 +00:00
const newValue = e.target.value
2025-09-15 09:31:47 +00:00
if (newValue !== selectedMaterialForStock) {
2025-09-15 21:11:40 +00:00
setSelectedMaterialForStock(newValue)
2025-09-15 09:31:47 +00:00
// Reset stock locations when material changes
2025-09-15 21:11:40 +00:00
setAvailableStockLocations([])
2025-09-15 09:31:47 +00:00
if (newValue) {
2025-09-15 21:11:40 +00:00
const stockLocations = generateStockLocationsForMaterial(newValue)
setAvailableStockLocations(stockLocations)
2025-09-15 09:31:47 +00:00
}
}
}}
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">
2025-09-15 21:11:40 +00:00
{selectedMaterial.totalStock} {selectedMaterial.baseUnit?.code}
2025-09-15 09:31:47 +00:00
</span>
</div>
<div>
<span className="text-gray-600">Birim:</span>
2025-09-15 21:11:40 +00:00
<span className="ml-1 font-medium">{selectedMaterial.baseUnit?.code}</span>
2025-09-15 09:31:47 +00:00
</div>
</div>
)}
</div>
{/* Lot Search */}
2025-09-15 21:11:40 +00:00
{selectedMaterialForStock && availableStockLocations.length > 0 && (
<LotSearchInput value={lotSearchTerm} onSearch={setLotSearchTerm} />
)}
2025-09-15 09:31:47 +00:00
{/* Stock Locations Section */}
2025-09-15 21:11:40 +00:00
{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>
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
<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}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
<div className="text-gray-500 text-xs">
{location.warehouseName}
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 21:11:40 +00:00
</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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
</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>
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
<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
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 21:11:40 +00:00
<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>
</>
)}
2025-09-15 09:31:47 +00:00
{/* 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>,
2025-09-15 21:11:40 +00:00
document.body,
)
}
2025-09-15 09:31:47 +00:00
return (
2025-09-15 21:11:40 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
{/* 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"
/>
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
<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"
2025-09-15 09:31:47 +00:00
/>
</div>
2025-09-15 21:11:40 +00:00
{/* 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>
2025-09-17 09:46:58 +00:00
{Object.values(MovementStatusEnum).map((status) => (
<option key={status} value={status}>
{getMovementStatusText(status)}
</option>
))}
2025-09-15 21:11:40 +00:00
</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) => {
2025-09-17 09:46:58 +00:00
const StatusIcon = getMovementStatusIcon(movement.status!)
2025-09-15 21:11:40 +00:00
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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
</td>
<td className="px-3 py-2 whitespace-nowrap">
2025-09-15 09:31:47 +00:00
<div>
<div className="text-sm font-medium text-gray-900">
2025-09-15 21:11:40 +00:00
{movement.toWarehouse?.name || movement.toWarehouse?.code}
2025-09-15 09:31:47 +00:00
</div>
<div className="text-sm text-gray-500">
2025-09-15 21:11:40 +00:00
{movement.toZone?.name && `${movement.toZone.name}`}
{movement.toLocation?.name && ` / ${movement.toLocation.name}`}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
{movement.toLocation?.locationCode && (
<div className="text-xs text-gray-400">
Kod: {movement.toLocation.locationCode}
</div>
)}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="text-sm text-gray-900">
{movement.quantity} {movement.unit?.code}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
{movement.lotNumber && (
<div className="text-sm text-gray-500">Lot: {movement.lotNumber}</div>
2025-09-15 09:31:47 +00:00
)}
2025-09-15 21:11:40 +00:00
</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
2025-09-17 09:46:58 +00:00
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMovementStatusColor(
2025-09-15 21:11:40 +00:00
movement.status!,
)}`}
2025-09-15 09:31:47 +00:00
>
2025-09-15 21:11:40 +00:00
<StatusIcon className="w-3 h-3 mr-1" />
2025-09-17 09:46:58 +00:00
{getMovementStatusText(movement.status!)}
2025-09-15 21:11:40 +00:00
</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>
2025-09-15 09:31:47 +00:00
</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={() => {
2025-09-15 21:11:40 +00:00
setShowIssueForm(false)
resetForm()
2025-09-15 09:31:47 +00:00
}}
/>
<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">
2025-09-15 21:11:40 +00:00
{editingMovement ? 'Stok Çıkış Düzenle' : 'Yeni Stok Çıkış'}
2025-09-15 09:31:47 +00:00
</h3>
<button
onClick={() => {
2025-09-15 21:11:40 +00:00
setShowIssueForm(false)
resetForm()
2025-09-15 09:31:47 +00:00
}}
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">
2025-09-15 21:11:40 +00:00
<h4 className="text-sm font-medium text-gray-900 mb-3">Genel Bilgiler</h4>
2025-09-15 09:31:47 +00:00
<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}
2025-09-15 21:11:40 +00:00
onChange={(e) => handleHeaderInputChange('movementNumber', e.target.value)}
2025-09-15 09:31:47 +00:00
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}
2025-09-15 21:11:40 +00:00
onChange={(e) => handleHeaderInputChange('movementDate', e.target.value)}
2025-09-15 09:31:47 +00:00
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) =>
2025-09-15 21:11:40 +00:00
handleHeaderInputChange('referenceDocument', e.target.value)
2025-09-15 09:31:47 +00:00
}
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}
2025-09-15 21:11:40 +00:00
onChange={(e) => handleHeaderInputChange('referenceType', e.target.value)}
2025-09-15 09:31:47 +00:00
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}
2025-09-15 21:11:40 +00:00
onChange={(e) => handleHeaderInputChange('performedBy', e.target.value)}
2025-09-15 09:31:47 +00:00
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">
2025-09-15 21:11:40 +00:00
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
2025-09-15 09:31:47 +00:00
<select
value={headerData.status}
onChange={(e) =>
2025-09-15 21:11:40 +00:00
handleHeaderInputChange('status', e.target.value as MovementStatusEnum)
2025-09-15 09:31:47 +00:00
}
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"
>
2025-09-17 09:46:58 +00:00
{Object.values(MovementStatusEnum).map((status) => (
<option key={status} value={status}>
{getMovementStatusText(status)}
</option>
))}
2025-09-15 09:31:47 +00:00
</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}
2025-09-15 21:11:40 +00:00
onChange={(e) => handleHeaderInputChange('description', e.target.value)}
2025-09-15 09:31:47 +00:00
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" />
2025-09-15 21:11:40 +00:00
<p className="text-sm font-medium">Henüz malzeme seçilmedi</p>
2025-09-15 09:31:47 +00:00
<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(
2025-09-15 21:11:40 +00:00
(m) => m.id === materialRow.materialId,
)
const totalQuantity = materialRow.selectedStockLocations.reduce(
(sum, loc) => sum + loc.requestedQuantity,
0,
)
2025-09-15 09:31:47 +00:00
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">
2025-09-15 21:11:40 +00:00
{totalQuantity} {selectedMaterial?.baseUnit?.code}
2025-09-15 09:31:47 +00:00
</div>
<div className="text-xs text-gray-500">
2025-09-15 21:11:40 +00:00
{materialRow.selectedStockLocations.length} lot
2025-09-15 09:31:47 +00:00
</div>
</div>
<div className="flex space-x-1">
<button
2025-09-15 21:11:40 +00:00
onClick={() => removeMaterial(materialRow.id)}
2025-09-15 09:31:47 +00:00
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 */}
2025-09-15 21:11:40 +00:00
{materialRow.selectedStockLocations.length > 0 && (
2025-09-15 09:31:47 +00:00
<div className="mt-3 pt-3 border-t border-gray-100">
<div className="space-y-2">
2025-09-15 21:11:40 +00:00
{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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 21:11:40 +00:00
</div>
))}
2025-09-15 09:31:47 +00:00
</div>
<div className="mt-2 text-xs text-gray-600 text-right">
2025-09-15 21:11:40 +00:00
Toplam Çıkış:{' '}
2025-09-15 09:31:47 +00:00
<span className="font-medium text-red-600">
{materialRow.selectedStockLocations.reduce(
2025-09-15 21:11:40 +00:00
(sum, loc) => sum + (loc.requestedQuantity || 0),
0,
)}{' '}
2025-09-15 09:31:47 +00:00
{selectedMaterial?.baseUnit?.code}
</span>
</div>
</div>
)}
</div>
2025-09-15 21:11:40 +00:00
)
2025-09-15 09:31:47 +00:00
})}
</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={() => {
2025-09-15 21:11:40 +00:00
setShowIssueForm(false)
resetForm()
2025-09-15 09:31:47 +00:00
}}
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" />
2025-09-15 21:11:40 +00:00
{editingMovement ? 'Güncelle' : 'Kaydet'}
2025-09-15 09:31:47 +00:00
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
2025-09-15 21:11:40 +00:00
</Container>
)
}
2025-09-15 09:31:47 +00:00
2025-09-15 21:11:40 +00:00
export default WarehouseIssue