erp-platform/ui/src/views/warehouse/components/LocationTracking.tsx
2025-09-16 00:11:40 +03:00

491 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react'
import {
FaSearch,
FaMapMarkerAlt,
FaExclamationTriangle,
FaCheckCircle,
FaEye,
FaTh,
FaList,
} from 'react-icons/fa'
import { mockWarehouses } from '../../../mocks/mockWarehouses'
import { mockLocations } from '../../../mocks/mockLocations'
import { mockStockItems } from '../../../mocks/mockStockItems'
import { getStockStatusColor, getStockStatusText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const LocationTracking: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [selectedWarehouse, setSelectedWarehouse] = useState<string>('')
const [selectedLocation, setSelectedLocation] = useState<string>('')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const getLocationUtilization = (locationId: string) => {
const location = mockLocations.find((l) => l.id === locationId)
if (!location) return 0
return (location.currentStock / location.capacity) * 100
}
const getLocationStockItems = (locationId: string) => {
return mockStockItems.filter((item) => item.locationId === locationId)
}
const filteredLocations = mockLocations.filter((location) => {
const matchesSearch =
location.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
location.locationCode.toLowerCase().includes(searchTerm.toLowerCase())
const matchesWarehouse = selectedWarehouse === '' || location.warehouseId === selectedWarehouse
return matchesSearch && matchesWarehouse
})
const GridView = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredLocations.map((location) => {
const warehouse = mockWarehouses.find((w) => w.id === location.warehouseId)
const locationStockItems = getLocationStockItems(location.id)
const utilization = getLocationUtilization(location.id)
return (
<div
key={location.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-3"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<FaMapMarkerAlt className="w-5 h-5 text-blue-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">{location.name}</h3>
<p className="text-sm text-gray-500">{location.locationCode}</p>
</div>
</div>
<button
onClick={() => setSelectedLocation(location.id)}
className="text-blue-600 hover:text-blue-700"
>
<FaEye className="w-4 h-4" />
</button>
</div>
<div className="space-y-3 pt-2">
<div className="text-sm text-gray-600">
<p>
<strong>Depo:</strong> {warehouse?.name}
</p>
<p>{location.description}</p>
</div>
{/* Utilization */}
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Doluluk Oranı</span>
<span className="font-medium">%{Math.round(utilization)}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
utilization > 90
? 'bg-red-500'
: utilization > 70
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{ width: `${utilization}%` }}
/>
</div>
<div className="text-xs text-gray-500">
{location.currentStock} / {location.capacity} birim
</div>
</div>
{/* Stock Items Summary */}
<div className="bg-gray-50 rounded-lg p-2">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-900">Malzemeler</h4>
<span className="text-xs text-gray-500">{locationStockItems.length} çeşit</span>
</div>
{locationStockItems.length > 0 ? (
<div className="space-y-1">
{locationStockItems.slice(0, 3).map((item) => (
<div key={item.id} className="flex justify-between items-center text-xs">
<span className="text-gray-700 truncate">
{item.material?.code || 'N/A'}
</span>
<div className="flex items-center gap-2">
<span className="font-medium">{item.quantity}</span>
<span
className={`px-1.5 py-0.5 rounded-full text-xs ${getStockStatusColor(
item.status,
)}`}
>
{getStockStatusText(item.status)}
</span>
</div>
</div>
))}
{locationStockItems.length > 3 && (
<div className="text-xs text-gray-500 text-center pt-1">
+{locationStockItems.length - 3} malzeme daha
</div>
)}
</div>
) : (
<div className="text-xs text-gray-500 text-center py-2">Malzeme bulunmuyor</div>
)}
</div>
{/* Restrictions */}
{location.restrictions && location.restrictions.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-1">Kısıtlamalar</h4>
<div className="flex flex-wrap gap-1">
{location.restrictions.slice(0, 2).map((restriction, index) => (
<span
key={index}
className="inline-flex px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded"
>
{restriction}
</span>
))}
{location.restrictions.length > 2 && (
<span className="inline-flex px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
+{location.restrictions.length - 2}
</span>
)}
</div>
</div>
)}
</div>
<div className="flex items-center justify-between pt-3 mt-3 border-t border-gray-100">
<div className="flex items-center gap-2">
{location.isActive ? (
<>
<FaCheckCircle className="w-4 h-4 text-green-500" />
<span className="text-sm text-green-600">Aktif</span>
</>
) : (
<>
<FaExclamationTriangle className="w-4 h-4 text-red-500" />
<span className="text-sm text-red-600">Pasif</span>
</>
)}
</div>
<div className="text-xs text-gray-500">
Son hareket:{' '}
{locationStockItems.length > 0
? new Date(
Math.max(
...locationStockItems.map((item) => item.lastMovementDate.getTime()),
),
).toLocaleDateString('tr-TR')
: 'N/A'}
</div>
</div>
</div>
)
})}
</div>
)
const ListView = () => (
<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">
Lokasyon
</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">
Doluluk
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Malzeme Çeşidi
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Son Hareket
</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">
{filteredLocations.map((location) => {
const warehouse = mockWarehouses.find((w) => w.id === location.warehouseId)
const locationStockItems = getLocationStockItems(location.id)
const utilization = getLocationUtilization(location.id)
return (
<tr key={location.id} className="hover:bg-gray-50">
<td className="px-3 py-2 whitespace-nowrap">
<div className="flex items-center">
<div className="p-2 bg-blue-100 rounded-lg mr-3">
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
</div>
<div>
<div className="text-sm font-medium text-gray-900">{location.name}</div>
<div className="text-sm text-gray-500">{location.locationCode}</div>
</div>
</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="text-sm text-gray-900">{warehouse?.name}</div>
<div className="text-sm text-gray-500">{warehouse?.code}</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="flex items-center">
<div className="w-16 bg-gray-200 rounded-full h-2 mr-2">
<div
className={`h-2 rounded-full ${
utilization > 90
? 'bg-red-500'
: utilization > 70
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{ width: `${utilization}%` }}
/>
</div>
<span className="text-sm text-gray-900">%{Math.round(utilization)}</span>
</div>
<div className="text-xs text-gray-500">
{location.currentStock} / {location.capacity}
</div>
</td>
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
{locationStockItems.length} çeşit
</td>
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
{locationStockItems.length > 0
? new Date(
Math.max(
...locationStockItems.map((item) => item.lastMovementDate.getTime()),
),
).toLocaleDateString('tr-TR')
: 'N/A'}
</td>
<td className="px-3 py-2 whitespace-nowrap">
{location.isActive ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<FaCheckCircle className="w-3 h-3 mr-1" />
Aktif
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<FaExclamationTriangle className="w-3 h-3 mr-1" />
Pasif
</span>
)}
</td>
<td className="px-3 py-2 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => setSelectedLocation(location.id)}
className="text-blue-600 hover:text-blue-900"
>
<FaEye className="w-4 h-4" />
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
const LocationDetailModal = () => {
const location = mockLocations.find((l) => l.id === selectedLocation)
const locationStockItems = getLocationStockItems(selectedLocation)
if (!selectedLocation || !location) return null
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={() => setSelectedLocation('')}
/>
<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 sm:max-w-4xl sm:w-full">
<div className="bg-white px-4 pt-4 pb-4 sm:p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">{location.name} - Detaylar</h3>
<button
onClick={() => setSelectedLocation('')}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Location Info */}
<div className="bg-gray-50 rounded-lg p-3">
<h4 className="font-medium text-gray-900 mb-3">Lokasyon Bilgileri</h4>
<div className="space-y-2 text-sm">
<div>
<strong>Kod:</strong> {location.locationCode}
</div>
<div>
<strong>ıklama:</strong> {location.description}
</div>
<div>
<strong>Tip:</strong> {location.locationType}
</div>
<div>
<strong>Kapasite:</strong> {location.capacity} birim
</div>
<div>
<strong>Mevcut Stok:</strong> {location.currentStock} birim
</div>
{location.dimensions && (
<div>
<strong>Boyutlar:</strong> {location.dimensions.length}x
{location.dimensions.width}x{location.dimensions.height}m
</div>
)}
</div>
</div>
{/* Stock Items */}
<div className="bg-gray-50 rounded-lg p-3">
<h4 className="font-medium text-gray-900 mb-3">
Stok Malzemeleri ({locationStockItems.length})
</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{locationStockItems.map((item) => (
<div key={item.id} className="bg-white rounded p-3 border">
<div className="flex justify-between items-start mb-2">
<div>
<div className="font-medium text-sm">{item.material?.code}</div>
<div className="text-xs text-gray-500">{item.material?.code}</div>
</div>
<span
className={`px-2 py-1 rounded-full text-xs ${getStockStatusColor(
item.status,
)}`}
>
{getStockStatusText(item.status)}
</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-gray-600">
<div>
Miktar: {item.quantity} {item.unitId}
</div>
<div>
Mevcut: {item.availableQuantity} {item.unitId}
</div>
<div>
Rezerve: {item.reservedQuantity} {item.unitId}
</div>
<div>Lot: {item.lotNumber || 'N/A'}</div>
</div>
<div className="mt-2 text-xs text-gray-500">
Son hareket: {item.lastMovementDate.toLocaleDateString('tr-TR')}
</div>
</div>
))}
{locationStockItems.length === 0 && (
<div className="text-sm text-gray-500 text-center py-4">
Bu lokasyonda malzeme bulunmuyor
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
return (
<Container>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Raf/Lokasyon Bazlı Takip</h2>
<p className="text-gray-600">Lokasyonlardaki stok durumunu takip edin</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode('grid')}
className={`p-1.5 rounded-lg ${
viewMode === 'grid'
? 'bg-blue-100 text-blue-600'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<FaTh className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded-lg ${
viewMode === 'list'
? 'bg-blue-100 text-blue-600'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<FaList className="w-4 h-4" />
</button>
</div>
</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="Lokasyon 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-blue-500 focus:border-transparent"
/>
</div>
<select
value={selectedWarehouse}
onChange={(e) => setSelectedWarehouse(e.target.value)}
className="px-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Tüm Depolar</option>
{mockWarehouses.map((warehouse) => (
<option key={warehouse.id} value={warehouse.id}>
{warehouse.name} ({warehouse.code})
</option>
))}
</select>
</div>
{/* Content */}
{viewMode === 'grid' ? <GridView /> : <ListView />}
</div>
{/* Location Detail Modal */}
<LocationDetailModal />
</Container>
)
}
export default LocationTracking