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

548 lines
22 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
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";
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 (
<div className="space-y-4 pt-2">
<div className="flex items-center justify-between">
<div>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">
2025-09-15 09:31:47 +00:00
Raf/Lokasyon Bazlı Takip
</h2>
2025-09-15 21:02:48 +00:00
<p className="text-gray-600">
2025-09-15 09:31:47 +00:00
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 />}
{/* Location Detail Modal */}
<LocationDetailModal />
</div>
);
};
export default LocationTracking;