Warehouse Management
This commit is contained in:
parent
8b88970fe2
commit
bb2e39c2c3
13 changed files with 4751 additions and 5982 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaBox,
|
FaBox,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
|
|
@ -8,521 +8,474 @@ import {
|
||||||
FaEye,
|
FaEye,
|
||||||
FaEdit,
|
FaEdit,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
// using modals for create actions; no navigation needed here
|
// using modals for create actions; no navigation needed here
|
||||||
import {
|
import { MmLotNumber, MmSerialNumber, QualityStatusEnum, SerialStatusEnum } from '../../../types/mm'
|
||||||
MmLotNumber,
|
import LotForm from './LotForm'
|
||||||
MmSerialNumber,
|
import SerialForm from './SerialForm'
|
||||||
QualityStatusEnum,
|
import { mockLotNumbers } from '../../../mocks/mockLotNumbers'
|
||||||
SerialStatusEnum,
|
import { mockSerialNumbers } from '../../../mocks/mockSerialNumbers'
|
||||||
} from "../../../types/mm";
|
import { mockMaterials } from '../../../mocks/mockMaterials'
|
||||||
import LotForm from "./LotForm";
|
import { mockUnits } from '../../../mocks/mockUnits'
|
||||||
import SerialForm from "./SerialForm";
|
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||||
import { mockLotNumbers } from "../../../mocks/mockLotNumbers";
|
import Widget from '../../../components/common/Widget'
|
||||||
import { mockSerialNumbers } from "../../../mocks/mockSerialNumbers";
|
import { PartyType } from '../../../types/common'
|
||||||
import { mockMaterials } from "../../../mocks/mockMaterials";
|
import { getQualityStatusInfo, getSerialStatusInfo } from '../../../utils/erp'
|
||||||
import { mockUnits } from "../../../mocks/mockUnits";
|
import { Container } from '@/components/shared'
|
||||||
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
|
||||||
import Widget from "../../../components/common/Widget";
|
|
||||||
import { PartyType } from "../../../types/common";
|
|
||||||
import { getQualityStatusInfo, getSerialStatusInfo } from "../../../utils/erp";
|
|
||||||
|
|
||||||
const InventoryTracking: React.FC = () => {
|
const InventoryTracking: React.FC = () => {
|
||||||
const [activeTab, setActiveTab] = useState<"lots" | "serials">("lots");
|
const [activeTab, setActiveTab] = useState<'lots' | 'serials'>('lots')
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [showCreateMenu, setShowCreateMenu] = useState(false);
|
const [showCreateMenu, setShowCreateMenu] = useState(false)
|
||||||
|
|
||||||
// Mock data için
|
// Mock data için
|
||||||
const [lotNumbers, setLotNumbers] = useState<MmLotNumber[]>(mockLotNumbers);
|
const [lotNumbers, setLotNumbers] = useState<MmLotNumber[]>(mockLotNumbers)
|
||||||
|
|
||||||
const [serialNumbers, setSerialNumbers] =
|
const [serialNumbers, setSerialNumbers] = useState<MmSerialNumber[]>(mockSerialNumbers)
|
||||||
useState<MmSerialNumber[]>(mockSerialNumbers);
|
|
||||||
|
|
||||||
// Modal control state
|
// Modal control state
|
||||||
const [openLotModal, setOpenLotModal] = useState(false);
|
const [openLotModal, setOpenLotModal] = useState(false)
|
||||||
const [openSerialModal, setOpenSerialModal] = useState(false);
|
const [openSerialModal, setOpenSerialModal] = useState(false)
|
||||||
const [currentLot, setCurrentLot] = useState<MmLotNumber | null>(null);
|
const [currentLot, setCurrentLot] = useState<MmLotNumber | null>(null)
|
||||||
const [currentSerial, setCurrentSerial] = useState<MmSerialNumber | null>(
|
const [currentSerial, setCurrentSerial] = useState<MmSerialNumber | null>(null)
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredLots = lotNumbers.filter(
|
const filteredLots = lotNumbers.filter(
|
||||||
(lot) =>
|
(lot) =>
|
||||||
lot.lotNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
lot.lotNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
lot.material?.name.toLowerCase().includes(searchTerm.toLowerCase())
|
lot.material?.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
)
|
||||||
|
|
||||||
const filteredSerials = serialNumbers.filter(
|
const filteredSerials = serialNumbers.filter(
|
||||||
(serial) =>
|
(serial) =>
|
||||||
serial.serialNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
serial.serialNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
serial.material?.name.toLowerCase().includes(searchTerm.toLowerCase())
|
serial.material?.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
);
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 py-2">
|
<Container>
|
||||||
{/* Header */}
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
{/* Header */}
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Envanter Takibi</h2>
|
<div>
|
||||||
<p className="text-gray-600">
|
<h2 className="text-2xl font-bold text-gray-900">Envanter Takibi</h2>
|
||||||
Lot ve seri numarası takiplerini yönetin
|
<p className="text-gray-600">Lot ve seri numarası takiplerini yönetin</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div className="relative">
|
||||||
<div className="relative">
|
<button
|
||||||
<button
|
onClick={() => setShowCreateMenu((s) => !s)} /* Kontrol küçültüldü */
|
||||||
onClick={() =>
|
className="bg-blue-600 text-white px-2.5 py-1 rounded-md hover:bg-blue-700 flex items-center space-x-1.5 text-sm"
|
||||||
setShowCreateMenu((s) => !s)
|
>
|
||||||
} /* Kontrol küçültüldü */
|
<FaPlus className="h-4 w-4" />
|
||||||
className="bg-blue-600 text-white px-2.5 py-1 rounded-md hover:bg-blue-700 flex items-center space-x-1.5 text-sm"
|
<span>Yeni Kayıt</span>
|
||||||
>
|
</button>
|
||||||
<FaPlus className="h-4 w-4" />
|
|
||||||
<span>Yeni Kayıt</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* only modal-based create menu (route-based entries removed) */}
|
{/* only modal-based create menu (route-based entries removed) */}
|
||||||
{showCreateMenu && (
|
{showCreateMenu && (
|
||||||
<div className="absolute right-0 mt-2 w-44 bg-white border rounded shadow-lg z-10">
|
<div className="absolute right-0 mt-2 w-44 bg-white border rounded shadow-lg z-10">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenLotModal(true);
|
setOpenLotModal(true)
|
||||||
setShowCreateMenu(false);
|
setShowCreateMenu(false)
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
|
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Yeni Lot
|
Yeni Lot
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenSerialModal(true);
|
setOpenSerialModal(true)
|
||||||
setShowCreateMenu(false);
|
setShowCreateMenu(false)
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
|
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
|
||||||
>
|
>
|
||||||
Yeni Seri
|
Yeni Seri
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary Statistics */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
|
||||||
<Widget
|
|
||||||
title="Toplam Lot"
|
|
||||||
value={lotNumbers.length}
|
|
||||||
color="blue"
|
|
||||||
icon="FaBox"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
|
||||||
title="Onaylı Lot"
|
|
||||||
value={
|
|
||||||
lotNumbers.filter(
|
|
||||||
(lot) => lot.qualityStatus === QualityStatusEnum.Approved
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
color="green"
|
|
||||||
icon="FaBox"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
|
||||||
title="Toplam Seri"
|
|
||||||
value={serialNumbers.length}
|
|
||||||
color="purple"
|
|
||||||
icon="FaHashtag"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Widget
|
|
||||||
title="Müsait Seri"
|
|
||||||
value={
|
|
||||||
serialNumbers.filter(
|
|
||||||
(serial) => serial.status === SerialStatusEnum.Available
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
color="orange"
|
|
||||||
icon="FaHashtag"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="relative">
|
|
||||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-3.5 w-3.5" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Lot/Seri numarası veya malzeme kodu ara..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="w-full pl-8 pr-3 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="border-b border-gray-200">
|
|
||||||
<nav className="flex -mb-px">
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("lots")}
|
|
||||||
className={`flex items-center px-2.5 py-1 text-sm font-medium border-b-2 ${
|
|
||||||
activeTab === "lots"
|
|
||||||
? "border-blue-500 text-blue-600"
|
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FaBox className="h-4 w-4 mr-2" />
|
|
||||||
Lot Numaraları ({filteredLots.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab("serials")}
|
|
||||||
className={`flex items-center px-2.5 py-1 text-sm font-medium border-b-2 ${
|
|
||||||
activeTab === "serials"
|
|
||||||
? "border-blue-500 text-blue-600"
|
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FaHashtag className="h-4 w-4 mr-2" />
|
|
||||||
Seri Numaraları ({filteredSerials.length})
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lot Numbers Tab */}
|
|
||||||
{activeTab === "lots" && (
|
|
||||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Lot Numarası
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Malzeme
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Miktar
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Üretim Tarihi
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Son Kullanma
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Tedarikçi
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Kalite Durumu
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 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">
|
|
||||||
{filteredLots.map((lot) => {
|
|
||||||
const qualityInfo = getQualityStatusInfo(lot.qualityStatus);
|
|
||||||
const isExpiringSoon =
|
|
||||||
lot.expiryDate &&
|
|
||||||
new Date(lot.expiryDate).getTime() - new Date().getTime() <
|
|
||||||
30 * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={lot.id} className="hover:bg-gray-50 text-xs">
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FaBox className="h-4 w-4 text-gray-400 mr-2" />
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{lot.lotNumber}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{lot.material?.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{lot.quantity} {lot.unitId}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FaCalendar className="h-4 w-4 text-gray-400 mr-2" />
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{new Date(lot.productionDate).toLocaleDateString(
|
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
{lot.expiryDate ? (
|
|
||||||
<div
|
|
||||||
className={`text-xs ${
|
|
||||||
isExpiringSoon
|
|
||||||
? "text-red-600 font-medium"
|
|
||||||
: "text-gray-900"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{new Date(lot.expiryDate).toLocaleDateString("tr-TR")}
|
|
||||||
{isExpiringSoon && (
|
|
||||||
<div className="text-xs text-red-500">
|
|
||||||
Yakında Dolacak
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400">-</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{lot.supplierId || "-"}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${qualityInfo.color}`}
|
|
||||||
>
|
|
||||||
{qualityInfo.label}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<div className="flex items-center justify-end space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentLot(lot);
|
|
||||||
setOpenLotModal(true);
|
|
||||||
}}
|
|
||||||
className="text-blue-600 hover:text-blue-900"
|
|
||||||
>
|
|
||||||
<FaEye className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentLot(lot);
|
|
||||||
setOpenLotModal(true);
|
|
||||||
}}
|
|
||||||
className="text-green-600 hover:text-green-900"
|
|
||||||
>
|
|
||||||
<FaEdit className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<LotForm
|
|
||||||
isOpen={openLotModal}
|
|
||||||
onClose={() => {
|
|
||||||
setOpenLotModal(false);
|
|
||||||
setCurrentLot(null);
|
|
||||||
}}
|
|
||||||
onSave={(lot) => setLotNumbers((prev) => [lot, ...prev])}
|
|
||||||
onUpdate={(updated) =>
|
|
||||||
setLotNumbers((prev) =>
|
|
||||||
prev.map((l) => (l.id === updated.id ? updated : l))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
materials={mockMaterials}
|
|
||||||
units={mockUnits}
|
|
||||||
suppliers={mockBusinessParties.filter(
|
|
||||||
(bp) => bp.partyType === PartyType.Supplier
|
|
||||||
)}
|
)}
|
||||||
initial={currentLot}
|
</div>
|
||||||
mode={currentLot ? "edit" : "create"}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{filteredLots.length === 0 && (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<FaBox className="mx-auto h-12 w-12 text-gray-400" />
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
|
||||||
Lot bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Arama kriterlerinize uygun lot kaydı bulunmuyor.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Serial Numbers Tab */}
|
{/* Summary Statistics */}
|
||||||
{activeTab === "serials" && (
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
<Widget title="Toplam Lot" value={lotNumbers.length} color="blue" icon="FaBox" />
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Seri Numarası
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Malzeme
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Lot
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Üretim Tarihi
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Garanti Bitiş
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Mevcut Lokasyon
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Durum
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 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">
|
|
||||||
{filteredSerials.map((serial) => {
|
|
||||||
const statusInfo = getSerialStatusInfo(serial.status);
|
|
||||||
const isWarrantyExpiring =
|
|
||||||
serial.warrantyExpiryDate &&
|
|
||||||
new Date(serial.warrantyExpiryDate).getTime() -
|
|
||||||
new Date().getTime() <
|
|
||||||
30 * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
return (
|
<Widget
|
||||||
<tr key={serial.id} className="hover:bg-gray-50 text-xs">
|
title="Onaylı Lot"
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
value={
|
||||||
<div className="flex items-center">
|
lotNumbers.filter((lot) => lot.qualityStatus === QualityStatusEnum.Approved).length
|
||||||
<FaHashtag className="h-4 w-4 text-gray-400 mr-2" />
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{serial.serialNumber}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{serial.material?.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{serial.lotId ? (
|
|
||||||
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">
|
|
||||||
LOT-{serial.lotId}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
"-"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FaCalendar className="h-4 w-4 text-gray-400 mr-2" />
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{new Date(serial.productionDate).toLocaleDateString(
|
|
||||||
"tr-TR"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
{serial.warrantyExpiryDate ? (
|
|
||||||
<div
|
|
||||||
className={`text-xs ${
|
|
||||||
isWarrantyExpiring
|
|
||||||
? "text-red-600 font-medium"
|
|
||||||
: "text-gray-900"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{new Date(
|
|
||||||
serial.warrantyExpiryDate
|
|
||||||
).toLocaleDateString("tr-TR")}
|
|
||||||
{isWarrantyExpiring && (
|
|
||||||
<div className="text-xs text-red-500">
|
|
||||||
Yakında Dolacak
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400">-</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FaMapMarkerAlt className="h-4 w-4 text-gray-400 mr-2" />
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{serial.currentLocationId || "-"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${statusInfo.color}`}
|
|
||||||
>
|
|
||||||
{statusInfo.label}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<div className="flex items-center justify-end space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentSerial(serial);
|
|
||||||
setOpenSerialModal(true);
|
|
||||||
}}
|
|
||||||
className="text-blue-600 hover:text-blue-900"
|
|
||||||
>
|
|
||||||
<FaEye className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentSerial(serial);
|
|
||||||
setOpenSerialModal(true);
|
|
||||||
}}
|
|
||||||
className="text-green-600 hover:text-green-900"
|
|
||||||
>
|
|
||||||
<FaEdit className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<SerialForm
|
|
||||||
isOpen={openSerialModal}
|
|
||||||
onClose={() => {
|
|
||||||
setOpenSerialModal(false);
|
|
||||||
setCurrentSerial(null);
|
|
||||||
}}
|
|
||||||
onSave={(serial) => setSerialNumbers((prev) => [serial, ...prev])}
|
|
||||||
onUpdate={(updated) =>
|
|
||||||
setSerialNumbers((prev) =>
|
|
||||||
prev.map((s) => (s.id === updated.id ? updated : s))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
materials={mockMaterials}
|
color="green"
|
||||||
lots={lotNumbers.map((l) => ({ id: l.id, label: l.lotNumber }))}
|
icon="FaBox"
|
||||||
initial={currentSerial}
|
|
||||||
mode={currentSerial ? "edit" : "create"}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{filteredSerials.length === 0 && (
|
<Widget
|
||||||
<div className="text-center py-12">
|
title="Toplam Seri"
|
||||||
<FaHashtag className="mx-auto h-12 w-12 text-gray-400" />
|
value={serialNumbers.length}
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
color="purple"
|
||||||
Seri numarası bulunamadı
|
icon="FaHashtag"
|
||||||
</h3>
|
/>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Arama kriterlerinize uygun seri numarası bulunmuyor.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InventoryTracking;
|
<Widget
|
||||||
|
title="Müsait Seri"
|
||||||
|
value={
|
||||||
|
serialNumbers.filter((serial) => serial.status === SerialStatusEnum.Available).length
|
||||||
|
}
|
||||||
|
color="orange"
|
||||||
|
icon="FaHashtag"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-3.5 w-3.5" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Lot/Seri numarası veya malzeme kodu ara..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-8 pr-3 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="border-b border-gray-200">
|
||||||
|
<nav className="flex -mb-px">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('lots')}
|
||||||
|
className={`flex items-center px-2.5 py-1 text-sm font-medium border-b-2 ${
|
||||||
|
activeTab === 'lots'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaBox className="h-4 w-4 mr-2" />
|
||||||
|
Lot Numaraları ({filteredLots.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('serials')}
|
||||||
|
className={`flex items-center px-2.5 py-1 text-sm font-medium border-b-2 ${
|
||||||
|
activeTab === 'serials'
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaHashtag className="h-4 w-4 mr-2" />
|
||||||
|
Seri Numaraları ({filteredSerials.length})
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lot Numbers Tab */}
|
||||||
|
{activeTab === 'lots' && (
|
||||||
|
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Lot Numarası
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Malzeme
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Miktar
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Üretim Tarihi
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Son Kullanma
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Tedarikçi
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Kalite Durumu
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 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">
|
||||||
|
{filteredLots.map((lot) => {
|
||||||
|
const qualityInfo = getQualityStatusInfo(lot.qualityStatus)
|
||||||
|
const isExpiringSoon =
|
||||||
|
lot.expiryDate &&
|
||||||
|
new Date(lot.expiryDate).getTime() - new Date().getTime() <
|
||||||
|
30 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={lot.id} className="hover:bg-gray-50 text-xs">
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaBox className="h-4 w-4 text-gray-400 mr-2" />
|
||||||
|
<div className="text-xs font-medium text-gray-900">{lot.lotNumber}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{lot.material?.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{lot.quantity} {lot.unitId}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaCalendar className="h-4 w-4 text-gray-400 mr-2" />
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{new Date(lot.productionDate).toLocaleDateString('tr-TR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
{lot.expiryDate ? (
|
||||||
|
<div
|
||||||
|
className={`text-xs ${
|
||||||
|
isExpiringSoon ? 'text-red-600 font-medium' : 'text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{new Date(lot.expiryDate).toLocaleDateString('tr-TR')}
|
||||||
|
{isExpiringSoon && (
|
||||||
|
<div className="text-xs text-red-500">Yakında Dolacak</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs text-gray-900">{lot.supplierId || '-'}</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 text-xs font-medium rounded-full ${qualityInfo.color}`}
|
||||||
|
>
|
||||||
|
{qualityInfo.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentLot(lot)
|
||||||
|
setOpenLotModal(true)
|
||||||
|
}}
|
||||||
|
className="text-blue-600 hover:text-blue-900"
|
||||||
|
>
|
||||||
|
<FaEye className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentLot(lot)
|
||||||
|
setOpenLotModal(true)
|
||||||
|
}}
|
||||||
|
className="text-green-600 hover:text-green-900"
|
||||||
|
>
|
||||||
|
<FaEdit className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<LotForm
|
||||||
|
isOpen={openLotModal}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenLotModal(false)
|
||||||
|
setCurrentLot(null)
|
||||||
|
}}
|
||||||
|
onSave={(lot) => setLotNumbers((prev) => [lot, ...prev])}
|
||||||
|
onUpdate={(updated) =>
|
||||||
|
setLotNumbers((prev) => prev.map((l) => (l.id === updated.id ? updated : l)))
|
||||||
|
}
|
||||||
|
materials={mockMaterials}
|
||||||
|
units={mockUnits}
|
||||||
|
suppliers={mockBusinessParties.filter((bp) => bp.partyType === PartyType.Supplier)}
|
||||||
|
initial={currentLot}
|
||||||
|
mode={currentLot ? 'edit' : 'create'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredLots.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FaBox className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">Lot bulunamadı</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Arama kriterlerinize uygun lot kaydı bulunmuyor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Serial Numbers Tab */}
|
||||||
|
{activeTab === 'serials' && (
|
||||||
|
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Seri Numarası
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Malzeme
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Lot
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Üretim Tarihi
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Garanti Bitiş
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Mevcut Lokasyon
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Durum
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 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">
|
||||||
|
{filteredSerials.map((serial) => {
|
||||||
|
const statusInfo = getSerialStatusInfo(serial.status)
|
||||||
|
const isWarrantyExpiring =
|
||||||
|
serial.warrantyExpiryDate &&
|
||||||
|
new Date(serial.warrantyExpiryDate).getTime() - new Date().getTime() <
|
||||||
|
30 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={serial.id} className="hover:bg-gray-50 text-xs">
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaHashtag className="h-4 w-4 text-gray-400 mr-2" />
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{serial.serialNumber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{serial.material?.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{serial.lotId ? (
|
||||||
|
<span className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">
|
||||||
|
LOT-{serial.lotId}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaCalendar className="h-4 w-4 text-gray-400 mr-2" />
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{new Date(serial.productionDate).toLocaleDateString('tr-TR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
{serial.warrantyExpiryDate ? (
|
||||||
|
<div
|
||||||
|
className={`text-xs ${
|
||||||
|
isWarrantyExpiring ? 'text-red-600 font-medium' : 'text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{new Date(serial.warrantyExpiryDate).toLocaleDateString('tr-TR')}
|
||||||
|
{isWarrantyExpiring && (
|
||||||
|
<div className="text-xs text-red-500">Yakında Dolacak</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">-</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaMapMarkerAlt className="h-4 w-4 text-gray-400 mr-2" />
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{serial.currentLocationId || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 text-xs font-medium rounded-full ${statusInfo.color}`}
|
||||||
|
>
|
||||||
|
{statusInfo.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSerial(serial)
|
||||||
|
setOpenSerialModal(true)
|
||||||
|
}}
|
||||||
|
className="text-blue-600 hover:text-blue-900"
|
||||||
|
>
|
||||||
|
<FaEye className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentSerial(serial)
|
||||||
|
setOpenSerialModal(true)
|
||||||
|
}}
|
||||||
|
className="text-green-600 hover:text-green-900"
|
||||||
|
>
|
||||||
|
<FaEdit className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<SerialForm
|
||||||
|
isOpen={openSerialModal}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenSerialModal(false)
|
||||||
|
setCurrentSerial(null)
|
||||||
|
}}
|
||||||
|
onSave={(serial) => setSerialNumbers((prev) => [serial, ...prev])}
|
||||||
|
onUpdate={(updated) =>
|
||||||
|
setSerialNumbers((prev) => prev.map((s) => (s.id === updated.id ? updated : s)))
|
||||||
|
}
|
||||||
|
materials={mockMaterials}
|
||||||
|
lots={lotNumbers.map((l) => ({ id: l.id, label: l.lotNumber }))}
|
||||||
|
initial={currentSerial}
|
||||||
|
mode={currentSerial ? 'edit' : 'create'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredSerials.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FaHashtag className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">Seri numarası bulunamadı</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Arama kriterlerinize uygun seri numarası bulunmuyor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InventoryTracking
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
FaSearch,
|
FaSearch,
|
||||||
FaMapMarkerAlt,
|
FaMapMarkerAlt,
|
||||||
|
|
@ -7,45 +7,43 @@ import {
|
||||||
FaEye,
|
FaEye,
|
||||||
FaTh,
|
FaTh,
|
||||||
FaList,
|
FaList,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import { mockWarehouses } from "../../../mocks/mockWarehouses";
|
import { mockWarehouses } from '../../../mocks/mockWarehouses'
|
||||||
import { mockLocations } from "../../../mocks/mockLocations";
|
import { mockLocations } from '../../../mocks/mockLocations'
|
||||||
import { mockStockItems } from "../../../mocks/mockStockItems";
|
import { mockStockItems } from '../../../mocks/mockStockItems'
|
||||||
import { getStockStatusColor, getStockStatusText } from "../../../utils/erp";
|
import { getStockStatusColor, getStockStatusText } from '../../../utils/erp'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const LocationTracking: React.FC = () => {
|
const LocationTracking: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [selectedWarehouse, setSelectedWarehouse] = useState<string>("");
|
const [selectedWarehouse, setSelectedWarehouse] = useState<string>('')
|
||||||
const [selectedLocation, setSelectedLocation] = useState<string>("");
|
const [selectedLocation, setSelectedLocation] = useState<string>('')
|
||||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||||
|
|
||||||
const getLocationUtilization = (locationId: string) => {
|
const getLocationUtilization = (locationId: string) => {
|
||||||
const location = mockLocations.find((l) => l.id === locationId);
|
const location = mockLocations.find((l) => l.id === locationId)
|
||||||
if (!location) return 0;
|
if (!location) return 0
|
||||||
return (location.currentStock / location.capacity) * 100;
|
return (location.currentStock / location.capacity) * 100
|
||||||
};
|
}
|
||||||
|
|
||||||
const getLocationStockItems = (locationId: string) => {
|
const getLocationStockItems = (locationId: string) => {
|
||||||
return mockStockItems.filter((item) => item.locationId === locationId);
|
return mockStockItems.filter((item) => item.locationId === locationId)
|
||||||
};
|
}
|
||||||
|
|
||||||
const filteredLocations = mockLocations.filter((location) => {
|
const filteredLocations = mockLocations.filter((location) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
location.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
location.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
location.locationCode.toLowerCase().includes(searchTerm.toLowerCase());
|
location.locationCode.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
const matchesWarehouse =
|
const matchesWarehouse = selectedWarehouse === '' || location.warehouseId === selectedWarehouse
|
||||||
selectedWarehouse === "" || location.warehouseId === selectedWarehouse;
|
return matchesSearch && matchesWarehouse
|
||||||
return matchesSearch && matchesWarehouse;
|
})
|
||||||
});
|
|
||||||
|
|
||||||
const GridView = () => (
|
const GridView = () => (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{filteredLocations.map((location) => {
|
{filteredLocations.map((location) => {
|
||||||
const warehouse = mockWarehouses.find(
|
const warehouse = mockWarehouses.find((w) => w.id === location.warehouseId)
|
||||||
(w) => w.id === location.warehouseId
|
const locationStockItems = getLocationStockItems(location.id)
|
||||||
);
|
const utilization = getLocationUtilization(location.id)
|
||||||
const locationStockItems = getLocationStockItems(location.id);
|
|
||||||
const utilization = getLocationUtilization(location.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -59,9 +57,7 @@ const LocationTracking: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-900">{location.name}</h3>
|
<h3 className="font-medium text-gray-900">{location.name}</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">{location.locationCode}</p>
|
||||||
{location.locationCode}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -84,18 +80,16 @@ const LocationTracking: React.FC = () => {
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-gray-600">Doluluk Oranı</span>
|
<span className="text-gray-600">Doluluk Oranı</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">%{Math.round(utilization)}</span>
|
||||||
%{Math.round(utilization)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${
|
||||||
utilization > 90
|
utilization > 90
|
||||||
? "bg-red-500"
|
? 'bg-red-500'
|
||||||
: utilization > 70
|
: utilization > 70
|
||||||
? "bg-yellow-500"
|
? 'bg-yellow-500'
|
||||||
: "bg-green-500"
|
: 'bg-green-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${utilization}%` }}
|
style={{ width: `${utilization}%` }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -108,29 +102,22 @@ const LocationTracking: React.FC = () => {
|
||||||
{/* Stock Items Summary */}
|
{/* Stock Items Summary */}
|
||||||
<div className="bg-gray-50 rounded-lg p-2">
|
<div className="bg-gray-50 rounded-lg p-2">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h4 className="text-sm font-medium text-gray-900">
|
<h4 className="text-sm font-medium text-gray-900">Malzemeler</h4>
|
||||||
Malzemeler
|
<span className="text-xs text-gray-500">{locationStockItems.length} çeşit</span>
|
||||||
</h4>
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
{locationStockItems.length} çeşit
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{locationStockItems.length > 0 ? (
|
{locationStockItems.length > 0 ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{locationStockItems.slice(0, 3).map((item) => (
|
{locationStockItems.slice(0, 3).map((item) => (
|
||||||
<div
|
<div key={item.id} className="flex justify-between items-center text-xs">
|
||||||
key={item.id}
|
|
||||||
className="flex justify-between items-center text-xs"
|
|
||||||
>
|
|
||||||
<span className="text-gray-700 truncate">
|
<span className="text-gray-700 truncate">
|
||||||
{item.material?.code || "N/A"}
|
{item.material?.code || 'N/A'}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{item.quantity}</span>
|
<span className="font-medium">{item.quantity}</span>
|
||||||
<span
|
<span
|
||||||
className={`px-1.5 py-0.5 rounded-full text-xs ${getStockStatusColor(
|
className={`px-1.5 py-0.5 rounded-full text-xs ${getStockStatusColor(
|
||||||
item.status
|
item.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getStockStatusText(item.status)}
|
{getStockStatusText(item.status)}
|
||||||
|
|
@ -145,29 +132,23 @@ const LocationTracking: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs text-gray-500 text-center py-2">
|
<div className="text-xs text-gray-500 text-center py-2">Malzeme bulunmuyor</div>
|
||||||
Malzeme bulunmuyor
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Restrictions */}
|
{/* Restrictions */}
|
||||||
{location.restrictions && location.restrictions.length > 0 && (
|
{location.restrictions && location.restrictions.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-1">
|
<h4 className="text-sm font-medium text-gray-900 mb-1">Kısıtlamalar</h4>
|
||||||
Kısıtlamalar
|
|
||||||
</h4>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{location.restrictions
|
{location.restrictions.slice(0, 2).map((restriction, index) => (
|
||||||
.slice(0, 2)
|
<span
|
||||||
.map((restriction, index) => (
|
key={index}
|
||||||
<span
|
className="inline-flex px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded"
|
||||||
key={index}
|
>
|
||||||
className="inline-flex px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded"
|
{restriction}
|
||||||
>
|
</span>
|
||||||
{restriction}
|
))}
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{location.restrictions.length > 2 && (
|
{location.restrictions.length > 2 && (
|
||||||
<span className="inline-flex px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
|
<span className="inline-flex px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
|
||||||
+{location.restrictions.length - 2}
|
+{location.restrictions.length - 2}
|
||||||
|
|
@ -193,23 +174,21 @@ const LocationTracking: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
Son hareket:{" "}
|
Son hareket:{' '}
|
||||||
{locationStockItems.length > 0
|
{locationStockItems.length > 0
|
||||||
? new Date(
|
? new Date(
|
||||||
Math.max(
|
Math.max(
|
||||||
...locationStockItems.map((item) =>
|
...locationStockItems.map((item) => item.lastMovementDate.getTime()),
|
||||||
item.lastMovementDate.getTime()
|
),
|
||||||
)
|
).toLocaleDateString('tr-TR')
|
||||||
)
|
: 'N/A'}
|
||||||
).toLocaleDateString("tr-TR")
|
|
||||||
: "N/A"}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
const ListView = () => (
|
const ListView = () => (
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
||||||
|
|
@ -242,11 +221,9 @@ const LocationTracking: React.FC = () => {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{filteredLocations.map((location) => {
|
{filteredLocations.map((location) => {
|
||||||
const warehouse = mockWarehouses.find(
|
const warehouse = mockWarehouses.find((w) => w.id === location.warehouseId)
|
||||||
(w) => w.id === location.warehouseId
|
const locationStockItems = getLocationStockItems(location.id)
|
||||||
);
|
const utilization = getLocationUtilization(location.id)
|
||||||
const locationStockItems = getLocationStockItems(location.id);
|
|
||||||
const utilization = getLocationUtilization(location.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={location.id} className="hover:bg-gray-50">
|
<tr key={location.id} className="hover:bg-gray-50">
|
||||||
|
|
@ -256,22 +233,14 @@ const LocationTracking: React.FC = () => {
|
||||||
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
|
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">{location.name}</div>
|
||||||
{location.name}
|
<div className="text-sm text-gray-500">{location.locationCode}</div>
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{location.locationCode}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap">
|
<td className="px-3 py-2 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">{warehouse?.name}</div>
|
||||||
{warehouse?.name}
|
<div className="text-sm text-gray-500">{warehouse?.code}</div>
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
{warehouse?.code}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap">
|
<td className="px-3 py-2 whitespace-nowrap">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|
@ -279,17 +248,15 @@ const LocationTracking: React.FC = () => {
|
||||||
<div
|
<div
|
||||||
className={`h-2 rounded-full ${
|
className={`h-2 rounded-full ${
|
||||||
utilization > 90
|
utilization > 90
|
||||||
? "bg-red-500"
|
? 'bg-red-500'
|
||||||
: utilization > 70
|
: utilization > 70
|
||||||
? "bg-yellow-500"
|
? 'bg-yellow-500'
|
||||||
: "bg-green-500"
|
: 'bg-green-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ width: `${utilization}%` }}
|
style={{ width: `${utilization}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-gray-900">
|
<span className="text-sm text-gray-900">%{Math.round(utilization)}</span>
|
||||||
%{Math.round(utilization)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-500">
|
||||||
{location.currentStock} / {location.capacity}
|
{location.currentStock} / {location.capacity}
|
||||||
|
|
@ -302,12 +269,10 @@ const LocationTracking: React.FC = () => {
|
||||||
{locationStockItems.length > 0
|
{locationStockItems.length > 0
|
||||||
? new Date(
|
? new Date(
|
||||||
Math.max(
|
Math.max(
|
||||||
...locationStockItems.map((item) =>
|
...locationStockItems.map((item) => item.lastMovementDate.getTime()),
|
||||||
item.lastMovementDate.getTime()
|
),
|
||||||
)
|
).toLocaleDateString('tr-TR')
|
||||||
)
|
: 'N/A'}
|
||||||
).toLocaleDateString("tr-TR")
|
|
||||||
: "N/A"}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap">
|
<td className="px-3 py-2 whitespace-nowrap">
|
||||||
{location.isActive ? (
|
{location.isActive ? (
|
||||||
|
|
@ -331,44 +296,37 @@ const LocationTracking: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
const LocationDetailModal = () => {
|
const LocationDetailModal = () => {
|
||||||
const location = mockLocations.find((l) => l.id === selectedLocation);
|
const location = mockLocations.find((l) => l.id === selectedLocation)
|
||||||
const locationStockItems = getLocationStockItems(selectedLocation);
|
const locationStockItems = getLocationStockItems(selectedLocation)
|
||||||
|
|
||||||
if (!selectedLocation || !location) return null;
|
if (!selectedLocation || !location) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
<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="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||||||
onClick={() => setSelectedLocation("")}
|
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="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="bg-white px-4 pt-4 pb-4 sm:p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-lg font-medium text-gray-900">
|
<h3 className="text-lg font-medium text-gray-900">{location.name} - Detaylar</h3>
|
||||||
{location.name} - Detaylar
|
|
||||||
</h3>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedLocation("")}
|
onClick={() => setSelectedLocation('')}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
>
|
>
|
||||||
<svg
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
className="w-6 h-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
|
@ -382,9 +340,7 @@ const LocationTracking: React.FC = () => {
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{/* Location Info */}
|
{/* Location Info */}
|
||||||
<div className="bg-gray-50 rounded-lg p-3">
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
<h4 className="font-medium text-gray-900 mb-3">
|
<h4 className="font-medium text-gray-900 mb-3">Lokasyon Bilgileri</h4>
|
||||||
Lokasyon Bilgileri
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<strong>Kod:</strong> {location.locationCode}
|
<strong>Kod:</strong> {location.locationCode}
|
||||||
|
|
@ -399,14 +355,12 @@ const LocationTracking: React.FC = () => {
|
||||||
<strong>Kapasite:</strong> {location.capacity} birim
|
<strong>Kapasite:</strong> {location.capacity} birim
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Mevcut Stok:</strong> {location.currentStock}{" "}
|
<strong>Mevcut Stok:</strong> {location.currentStock} birim
|
||||||
birim
|
|
||||||
</div>
|
</div>
|
||||||
{location.dimensions && (
|
{location.dimensions && (
|
||||||
<div>
|
<div>
|
||||||
<strong>Boyutlar:</strong> {location.dimensions.length}x
|
<strong>Boyutlar:</strong> {location.dimensions.length}x
|
||||||
{location.dimensions.width}x{location.dimensions.height}
|
{location.dimensions.width}x{location.dimensions.height}m
|
||||||
m
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -419,22 +373,15 @@ const LocationTracking: React.FC = () => {
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||||
{locationStockItems.map((item) => (
|
{locationStockItems.map((item) => (
|
||||||
<div
|
<div key={item.id} className="bg-white rounded p-3 border">
|
||||||
key={item.id}
|
|
||||||
className="bg-white rounded p-3 border"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start mb-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-sm">
|
<div className="font-medium text-sm">{item.material?.code}</div>
|
||||||
{item.material?.code}
|
<div className="text-xs text-gray-500">{item.material?.code}</div>
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{item.material?.code}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 rounded-full text-xs ${getStockStatusColor(
|
className={`px-2 py-1 rounded-full text-xs ${getStockStatusColor(
|
||||||
item.status
|
item.status,
|
||||||
)}`}
|
)}`}
|
||||||
>
|
>
|
||||||
{getStockStatusText(item.status)}
|
{getStockStatusText(item.status)}
|
||||||
|
|
@ -450,11 +397,10 @@ const LocationTracking: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
Rezerve: {item.reservedQuantity} {item.unitId}
|
Rezerve: {item.reservedQuantity} {item.unitId}
|
||||||
</div>
|
</div>
|
||||||
<div>Lot: {item.lotNumber || "N/A"}</div>
|
<div>Lot: {item.lotNumber || 'N/A'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-xs text-gray-500">
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
Son hareket:{" "}
|
Son hareket: {item.lastMovementDate.toLocaleDateString('tr-TR')}
|
||||||
{item.lastMovementDate.toLocaleDateString("tr-TR")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -470,78 +416,76 @@ const LocationTracking: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pt-2">
|
<Container>
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-2">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">
|
<div>
|
||||||
Raf/Lokasyon Bazlı Takip
|
<h2 className="text-2xl font-bold text-gray-900">Raf/Lokasyon Bazlı Takip</h2>
|
||||||
</h2>
|
<p className="text-gray-600">Lokasyonlardaki stok durumunu takip edin</p>
|
||||||
<p className="text-gray-600">
|
</div>
|
||||||
Lokasyonlardaki stok durumunu takip edin
|
|
||||||
</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
{/* Filters */}
|
||||||
<button
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
onClick={() => setViewMode("grid")}
|
<div className="relative flex-1">
|
||||||
className={`p-1.5 rounded-lg ${
|
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
viewMode === "grid"
|
<input
|
||||||
? "bg-blue-100 text-blue-600"
|
type="text"
|
||||||
: "text-gray-400 hover:text-gray-600"
|
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"
|
||||||
>
|
>
|
||||||
<FaTh className="w-4 h-4" />
|
<option value="">Tüm Depolar</option>
|
||||||
</button>
|
{mockWarehouses.map((warehouse) => (
|
||||||
<button
|
<option key={warehouse.id} value={warehouse.id}>
|
||||||
onClick={() => setViewMode("list")}
|
{warehouse.name} ({warehouse.code})
|
||||||
className={`p-1.5 rounded-lg ${
|
</option>
|
||||||
viewMode === "list"
|
))}
|
||||||
? "bg-blue-100 text-blue-600"
|
</select>
|
||||||
: "text-gray-400 hover:text-gray-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<FaList className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Content */}
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
{viewMode === 'grid' ? <GridView /> : <ListView />}
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
{viewMode === "grid" ? <GridView /> : <ListView />}
|
|
||||||
|
|
||||||
{/* Location Detail Modal */}
|
{/* Location Detail Modal */}
|
||||||
<LocationDetailModal />
|
<LocationDetailModal />
|
||||||
</div>
|
</Container>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LocationTracking;
|
export default LocationTracking
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,26 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from "yup";
|
import * as Yup from 'yup'
|
||||||
import { FaSave, FaTimes } from "react-icons/fa";
|
import { FaSave, FaTimes } from 'react-icons/fa'
|
||||||
import {
|
import { QualityStatusEnum, MmLotNumber, MmUnit, MmMaterial } from '../../../types/mm'
|
||||||
QualityStatusEnum,
|
import { BusinessParty } from '../../../types/common'
|
||||||
MmLotNumber,
|
|
||||||
MmUnit,
|
|
||||||
MmMaterial,
|
|
||||||
} from "../../../types/mm";
|
|
||||||
import { BusinessParty } from "../../../types/common";
|
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
materialId: Yup.string().required("Malzeme seçimi zorunlu"),
|
materialId: Yup.string().required('Malzeme seçimi zorunlu'),
|
||||||
lotNumber: Yup.string().required("Lot numarası zorunlu"),
|
lotNumber: Yup.string().required('Lot numarası zorunlu'),
|
||||||
quantity: Yup.number().min(0, "Miktar 0'dan büyük olmalıdır"),
|
quantity: Yup.number().min(0, "Miktar 0'dan büyük olmalıdır"),
|
||||||
});
|
})
|
||||||
|
|
||||||
interface LotFormProps {
|
interface LotFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onSave: (lot: MmLotNumber) => void;
|
onSave: (lot: MmLotNumber) => void
|
||||||
onUpdate?: (lot: MmLotNumber) => void;
|
onUpdate?: (lot: MmLotNumber) => void
|
||||||
materials: MmMaterial[];
|
materials: MmMaterial[]
|
||||||
suppliers?: BusinessParty[];
|
suppliers?: BusinessParty[]
|
||||||
units?: MmUnit[];
|
units?: MmUnit[]
|
||||||
initial?: MmLotNumber | null;
|
initial?: MmLotNumber | null
|
||||||
mode?: "create" | "edit" | "view";
|
mode?: 'create' | 'edit' | 'view'
|
||||||
}
|
}
|
||||||
|
|
||||||
const LotForm: React.FC<LotFormProps> = ({
|
const LotForm: React.FC<LotFormProps> = ({
|
||||||
|
|
@ -37,17 +32,17 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
suppliers = [],
|
suppliers = [],
|
||||||
units = [],
|
units = [],
|
||||||
initial = null,
|
initial = null,
|
||||||
mode = "create",
|
mode = 'create',
|
||||||
}) => {
|
}) => {
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
materialId: "",
|
materialId: '',
|
||||||
lotNumber: "",
|
lotNumber: '',
|
||||||
productionDate: "",
|
productionDate: '',
|
||||||
expiryDate: "",
|
expiryDate: '',
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
unitId: "KG",
|
unitId: 'KG',
|
||||||
supplierId: suppliers.length ? suppliers[0].id : "",
|
supplierId: suppliers.length ? suppliers[0].id : '',
|
||||||
qualityStatus: QualityStatusEnum.Pending,
|
qualityStatus: QualityStatusEnum.Pending,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
|
|
@ -57,52 +52,48 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
id: (initial && initial.id) || Date.now().toString(),
|
id: (initial && initial.id) || Date.now().toString(),
|
||||||
materialId: values.materialId,
|
materialId: values.materialId,
|
||||||
lotNumber: values.lotNumber,
|
lotNumber: values.lotNumber,
|
||||||
productionDate: values.productionDate
|
productionDate: values.productionDate ? new Date(values.productionDate) : new Date(),
|
||||||
? new Date(values.productionDate)
|
|
||||||
: new Date(),
|
|
||||||
expiryDate: values.expiryDate ? new Date(values.expiryDate) : undefined,
|
expiryDate: values.expiryDate ? new Date(values.expiryDate) : undefined,
|
||||||
quantity: Number(values.quantity),
|
quantity: Number(values.quantity),
|
||||||
unitId: values.unitId,
|
unitId: values.unitId,
|
||||||
supplierId: values.supplierId || undefined,
|
supplierId: values.supplierId || undefined,
|
||||||
qualityStatus: values.qualityStatus,
|
qualityStatus: values.qualityStatus,
|
||||||
isActive: !!values.isActive,
|
isActive: !!values.isActive,
|
||||||
};
|
}
|
||||||
|
|
||||||
// simulate API
|
// simulate API
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300))
|
||||||
if (mode === "edit" && onUpdate) {
|
if (mode === 'edit' && onUpdate) {
|
||||||
onUpdate(newLot);
|
onUpdate(newLot)
|
||||||
} else {
|
} else {
|
||||||
onSave(newLot);
|
onSave(newLot)
|
||||||
}
|
}
|
||||||
onClose();
|
onClose()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// sync initial values when editing/viewing
|
// sync initial values when editing/viewing
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initial) {
|
if (initial) {
|
||||||
const src = initial;
|
const src = initial
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
materialId: src.materialId || "",
|
materialId: src.materialId || '',
|
||||||
lotNumber: src.lotNumber || "",
|
lotNumber: src.lotNumber || '',
|
||||||
productionDate: src.productionDate
|
productionDate: src.productionDate
|
||||||
? new Date(src.productionDate).toISOString().slice(0, 10)
|
? new Date(src.productionDate).toISOString().slice(0, 10)
|
||||||
: "",
|
: '',
|
||||||
expiryDate: src.expiryDate
|
expiryDate: src.expiryDate ? new Date(src.expiryDate).toISOString().slice(0, 10) : '',
|
||||||
? new Date(src.expiryDate).toISOString().slice(0, 10)
|
|
||||||
: "",
|
|
||||||
quantity: src.quantity || 0,
|
quantity: src.quantity || 0,
|
||||||
unitId: src.unitId || (units.length ? units[0].id : ""),
|
unitId: src.unitId || (units.length ? units[0].id : ''),
|
||||||
supplierId: src.supplierId || (suppliers.length ? suppliers[0].id : ""),
|
supplierId: src.supplierId || (suppliers.length ? suppliers[0].id : ''),
|
||||||
qualityStatus: src.qualityStatus || QualityStatusEnum.Pending,
|
qualityStatus: src.qualityStatus || QualityStatusEnum.Pending,
|
||||||
isActive: !!src.isActive,
|
isActive: !!src.isActive,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [initial]);
|
}, [initial])
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
|
@ -112,18 +103,18 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
<div className="flex items-center justify-between p-2 border-b">
|
<div className="flex items-center justify-between p-2 border-b">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900">
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
{mode === "create"
|
{mode === 'create'
|
||||||
? "Yeni Lot Kaydı"
|
? 'Yeni Lot Kaydı'
|
||||||
: mode === "edit"
|
: mode === 'edit'
|
||||||
? "Lot Düzenle"
|
? 'Lot Düzenle'
|
||||||
: "Lot Detayı"}
|
: 'Lot Detayı'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{mode === "create"
|
{mode === 'create'
|
||||||
? "Lot bilgilerini girin"
|
? 'Lot bilgilerini girin'
|
||||||
: mode === "edit"
|
: mode === 'edit'
|
||||||
? "Mevcut lot bilgilerini güncelleyin"
|
? 'Mevcut lot bilgilerini güncelleyin'
|
||||||
: "Lot bilgileri (sadece gösterim)"}
|
: 'Lot bilgileri (sadece gösterim)'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -134,7 +125,7 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
>
|
>
|
||||||
<FaTimes className="inline mr-1 h-3 w-3" /> Kapat
|
<FaTimes className="inline mr-1 h-3 w-3" /> Kapat
|
||||||
</button>
|
</button>
|
||||||
{mode !== "view" && (
|
{mode !== 'view' && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
|
className="px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
|
||||||
|
|
@ -147,12 +138,10 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 p-2 gap-x-3 gap-y-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 p-2 gap-x-3 gap-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Malzeme *</label>
|
||||||
Malzeme *
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("materialId")}
|
{...formik.getFieldProps('materialId')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Seçiniz...</option>
|
<option value="">Seçiniz...</option>
|
||||||
|
|
@ -165,61 +154,51 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Lot Numarası *</label>
|
||||||
Lot Numarası *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...formik.getFieldProps("lotNumber")}
|
{...formik.getFieldProps('lotNumber')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Üretim Tarihi</label>
|
||||||
Üretim Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
{...formik.getFieldProps("productionDate")}
|
{...formik.getFieldProps('productionDate')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Son Kullanma Tarihi</label>
|
||||||
Son Kullanma Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
{...formik.getFieldProps("expiryDate")}
|
{...formik.getFieldProps('expiryDate')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Miktar</label>
|
||||||
Miktar
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
{...formik.getFieldProps("quantity")}
|
{...formik.getFieldProps('quantity')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Birim</label>
|
||||||
Birim
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("unitId")}
|
{...formik.getFieldProps('unitId')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
{units.map((u) => (
|
{units.map((u) => (
|
||||||
|
|
@ -231,12 +210,10 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Tedarikçi</label>
|
||||||
Tedarikçi
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("supplierId")}
|
{...formik.getFieldProps('supplierId')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Seçiniz...</option>
|
<option value="">Seçiniz...</option>
|
||||||
|
|
@ -249,11 +226,9 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Kalite Durumu</label>
|
||||||
Kalite Durumu
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("qualityStatus")}
|
{...formik.getFieldProps('qualityStatus')}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
<option value={QualityStatusEnum.Pending}>Beklemede</option>
|
<option value={QualityStatusEnum.Pending}>Beklemede</option>
|
||||||
|
|
@ -266,7 +241,7 @@ const LotForm: React.FC<LotFormProps> = ({
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LotForm;
|
export default LotForm
|
||||||
|
|
|
||||||
|
|
@ -1,382 +1,346 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from 'react-router-dom'
|
||||||
import { FaDownload, FaCalendar, FaBox } from "react-icons/fa";
|
import { FaDownload, FaCalendar, FaBox } from 'react-icons/fa'
|
||||||
import { MmStockMovement, MovementTypeEnum } from "../../../types/mm";
|
import { MmStockMovement, MovementTypeEnum } from '../../../types/mm'
|
||||||
import { FaRepeat } from "react-icons/fa6";
|
import { FaRepeat } from 'react-icons/fa6'
|
||||||
import { getMovementTypeInfo } from "../../../utils/erp";
|
import { getMovementTypeInfo } from '../../../utils/erp'
|
||||||
import { mockStockMovements } from "../../../mocks/mockStockMovements";
|
import { mockStockMovements } from '../../../mocks/mockStockMovements'
|
||||||
import Widget, { colorType } from "../../../components/common/Widget";
|
import Widget, { colorType } from '../../../components/common/Widget'
|
||||||
|
import { Container } from '@/components/shared'
|
||||||
|
|
||||||
const MaterialMovements: React.FC<{ materialId?: string }> = ({
|
const MaterialMovements: React.FC<{ materialId?: string }> = ({ materialId }) => {
|
||||||
materialId,
|
const params = useParams<Record<string, string | undefined>>()
|
||||||
}) => {
|
const routeMaterialId = materialId || params.materialId || params.id || undefined
|
||||||
const params = useParams<Record<string, string | undefined>>();
|
const [movements, setMovements] = useState<MmStockMovement[]>([])
|
||||||
const routeMaterialId =
|
const [loading, setLoading] = useState(false)
|
||||||
materialId || params.materialId || params.id || undefined;
|
|
||||||
const [movements, setMovements] = useState<MmStockMovement[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
startDate: "",
|
startDate: '',
|
||||||
endDate: "",
|
endDate: '',
|
||||||
movementType: "",
|
movementType: '',
|
||||||
warehouseId: "",
|
warehouseId: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const loadMovements = useCallback(() => {
|
const loadMovements = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let filteredMovements = mockStockMovements;
|
let filteredMovements = mockStockMovements
|
||||||
|
|
||||||
const activeMaterialId = routeMaterialId;
|
const activeMaterialId = routeMaterialId
|
||||||
if (activeMaterialId) {
|
if (activeMaterialId) {
|
||||||
filteredMovements = filteredMovements.filter(
|
filteredMovements = filteredMovements.filter((m) => m.materialId === activeMaterialId)
|
||||||
(m) => m.materialId === activeMaterialId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.movementType) {
|
if (filters.movementType) {
|
||||||
filteredMovements = filteredMovements.filter(
|
filteredMovements = filteredMovements.filter(
|
||||||
(m) =>
|
(m) => m.movementType === (filters.movementType as unknown as MovementTypeEnum),
|
||||||
m.movementType ===
|
)
|
||||||
(filters.movementType as unknown as MovementTypeEnum)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setMovements(filteredMovements);
|
setMovements(filteredMovements)
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}, 1000);
|
}, 1000)
|
||||||
}, [routeMaterialId, filters]);
|
}, [routeMaterialId, filters])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadMovements();
|
loadMovements()
|
||||||
}, [loadMovements]);
|
}, [loadMovements])
|
||||||
|
|
||||||
const calculateRunningBalance = () => {
|
const calculateRunningBalance = () => {
|
||||||
let balance = 0;
|
let balance = 0
|
||||||
return movements
|
return movements
|
||||||
.sort(
|
.sort((a, b) => new Date(a.movementDate).getTime() - new Date(b.movementDate).getTime())
|
||||||
(a, b) =>
|
|
||||||
new Date(a.movementDate).getTime() -
|
|
||||||
new Date(b.movementDate).getTime()
|
|
||||||
)
|
|
||||||
.map((movement) => {
|
.map((movement) => {
|
||||||
balance += movement.quantity;
|
balance += movement.quantity
|
||||||
return { ...movement, runningBalance: balance };
|
return { ...movement, runningBalance: balance }
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const movementsWithBalance = calculateRunningBalance();
|
const movementsWithBalance = calculateRunningBalance()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 py-2">
|
<Container>
|
||||||
{/* Header */}
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
{/* Header */}
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">
|
<div>
|
||||||
{materialId ? "Stok Hareketleri" : "Tüm Stok Hareketleri"}
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
</h2>
|
{materialId ? 'Stok Hareketleri' : 'Tüm Stok Hareketleri'}
|
||||||
<p className="text-gray-600">
|
</h2>
|
||||||
{materialId
|
<p className="text-gray-600">
|
||||||
? `${materialId} kodlu malzeme için`
|
{materialId ? `${materialId} kodlu malzeme için` : 'Tüm malzemeler için'} stok
|
||||||
: "Tüm malzemeler için"}{" "}
|
hareketlerini görüntüleyin
|
||||||
stok hareketlerini görüntüleyin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-1.5">
|
|
||||||
<button
|
|
||||||
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")}
|
|
||||||
className="bg-gray-600 text-white px-2.5 py-1 rounded-md hover:bg-gray-700 flex items-center space-x-1.5 text-sm"
|
|
||||||
>
|
|
||||||
<FaDownload className="h-4 w-4" />
|
|
||||||
<span>Dışa Aktar</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={loadMovements}
|
|
||||||
className="bg-blue-600 text-white px-2.5 py-1 rounded-md hover:bg-blue-700 flex items-center space-x-1.5 text-sm"
|
|
||||||
>
|
|
||||||
<FaRepeat className="h-4 w-4" />
|
|
||||||
<span>Yenile</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
title: "Toplam Giriş",
|
|
||||||
value: "125 KG",
|
|
||||||
color: "green",
|
|
||||||
icon: "FaArrowDown",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Toplam Çıkış",
|
|
||||||
value: "50 KG",
|
|
||||||
color: "red",
|
|
||||||
icon: "FaArrowUp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Net Bakiye",
|
|
||||||
value: "75 KG",
|
|
||||||
color: "blue",
|
|
||||||
icon: "FaBalanceScale",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Hareket Sayısı",
|
|
||||||
value: movements.length,
|
|
||||||
color: "gray",
|
|
||||||
icon: "FaExchangeAlt",
|
|
||||||
},
|
|
||||||
].map((stat) => (
|
|
||||||
<Widget
|
|
||||||
key={stat.title}
|
|
||||||
title={stat.title}
|
|
||||||
value={stat.value}
|
|
||||||
color={stat.color as colorType}
|
|
||||||
icon={stat.icon}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="bg-white p-2 rounded-lg shadow-sm">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
||||||
Başlangıç Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={filters.startDate}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilters({ ...filters, startDate: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
||||||
Bitiş Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={filters.endDate}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilters({ ...filters, endDate: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
||||||
Hareket Tipi
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.movementType}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilters({ ...filters, movementType: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
|
||||||
<option value="">Tümü</option>
|
|
||||||
<option value={MovementTypeEnum.GoodsReceipt}>Mal Girişi</option>
|
|
||||||
<option value={MovementTypeEnum.GoodsIssue}>Mal Çıkışı</option>
|
|
||||||
<option value={MovementTypeEnum.Transfer}>Transfer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
||||||
Depo
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filters.warehouseId}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFilters({ ...filters, warehouseId: e.target.value })
|
|
||||||
}
|
|
||||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
|
||||||
<option value="">Tümü</option>
|
|
||||||
<option value="WH-001">Ana Depo</option>
|
|
||||||
<option value="WH-002">Üretim Deposu</option>
|
|
||||||
<option value="WH-003">Satış Deposu</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Movements Table */}
|
|
||||||
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
|
||||||
<p className="mt-2 text-gray-500">Hareketler yükleniyor...</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Tarih/Saat
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Hareket Tipi
|
|
||||||
</th>
|
|
||||||
{!materialId && (
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Malzeme
|
|
||||||
</th>
|
|
||||||
)}
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Miktar
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Bakiye
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Depo/Lokasyon
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Lot/Seri
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Belge
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Açıklama
|
|
||||||
</th>
|
|
||||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Oluşturan
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{movementsWithBalance.map((movement) => {
|
|
||||||
const typeInfo = getMovementTypeInfo(movement.movementType);
|
|
||||||
const Icon = typeInfo.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={movement.id} className="hover:bg-gray-50 text-xs">
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<FaCalendar className="h-4 w-4 text-gray-400" />
|
|
||||||
<div>
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{new Date(
|
|
||||||
movement.movementDate
|
|
||||||
).toLocaleDateString("tr-TR")}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{new Date(
|
|
||||||
movement.movementDate
|
|
||||||
).toLocaleTimeString("tr-TR")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div
|
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${typeInfo.color}`}
|
|
||||||
>
|
|
||||||
<Icon className="h-3 w-3 mr-1" />
|
|
||||||
{typeInfo.label}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{!materialId && (
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{movement.material.code} - {movement.material.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<span
|
|
||||||
className={`text-sm font-medium ${
|
|
||||||
movement.quantity > 0
|
|
||||||
? "text-green-600"
|
|
||||||
: "text-red-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{movement.quantity > 0 ? "+" : ""}
|
|
||||||
{movement.quantity}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
{movement.unit?.code}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{movement.runningBalance} {movement.unit?.code}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{movement.toWarehouse?.code}
|
|
||||||
{movement.toZone?.zoneCode && (
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{movement.toZone?.zoneCode}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{movement.toLocation?.locationCode && (
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{movement.toLocation?.locationCode}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{movement.lotNumber && (
|
|
||||||
<div className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
||||||
{movement.lotNumber}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
||||||
{movement.referenceDocument && (
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
<div>{movement.referenceDocument}</div>
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
{movement.referenceDocumentType}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-2 py-1.5">
|
|
||||||
<div className="text-xs text-gray-900">
|
|
||||||
{movement.description}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && movements.length === 0 && (
|
|
||||||
<div className="text-center py-10">
|
|
||||||
<FaBox className="mx-auto h-10 w-10 text-gray-400" />
|
|
||||||
<h3 className="mt-2 text-base font-medium text-gray-900">
|
|
||||||
Hareket bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-xs text-gray-500">
|
|
||||||
Seçilen kriterlere uygun hareket kaydı bulunmuyor.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MaterialMovements;
|
<div className="flex items-center space-x-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
|
||||||
|
className="bg-gray-600 text-white px-2.5 py-1 rounded-md hover:bg-gray-700 flex items-center space-x-1.5 text-sm"
|
||||||
|
>
|
||||||
|
<FaDownload className="h-4 w-4" />
|
||||||
|
<span>Dışa Aktar</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={loadMovements}
|
||||||
|
className="bg-blue-600 text-white px-2.5 py-1 rounded-md hover:bg-blue-700 flex items-center space-x-1.5 text-sm"
|
||||||
|
>
|
||||||
|
<FaRepeat className="h-4 w-4" />
|
||||||
|
<span>Yenile</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: 'Toplam Giriş',
|
||||||
|
value: '125 KG',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'FaArrowDown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Toplam Çıkış',
|
||||||
|
value: '50 KG',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'FaArrowUp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Net Bakiye',
|
||||||
|
value: '75 KG',
|
||||||
|
color: 'blue',
|
||||||
|
icon: 'FaBalanceScale',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Hareket Sayısı',
|
||||||
|
value: movements.length,
|
||||||
|
color: 'gray',
|
||||||
|
icon: 'FaExchangeAlt',
|
||||||
|
},
|
||||||
|
].map((stat) => (
|
||||||
|
<Widget
|
||||||
|
key={stat.title}
|
||||||
|
title={stat.title}
|
||||||
|
value={stat.value}
|
||||||
|
color={stat.color as colorType}
|
||||||
|
icon={stat.icon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="bg-white p-2 rounded-lg shadow-sm">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Başlangıç Tarihi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
|
||||||
|
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Bitiş Tarihi</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
|
||||||
|
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Hareket Tipi</label>
|
||||||
|
<select
|
||||||
|
value={filters.movementType}
|
||||||
|
onChange={(e) => setFilters({ ...filters, movementType: e.target.value })}
|
||||||
|
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="">Tümü</option>
|
||||||
|
<option value={MovementTypeEnum.GoodsReceipt}>Mal Girişi</option>
|
||||||
|
<option value={MovementTypeEnum.GoodsIssue}>Mal Çıkışı</option>
|
||||||
|
<option value={MovementTypeEnum.Transfer}>Transfer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="block text-xs font-medium text-gray-700 mb-1">Depo</label>
|
||||||
|
<select
|
||||||
|
value={filters.warehouseId}
|
||||||
|
onChange={(e) => setFilters({ ...filters, warehouseId: e.target.value })}
|
||||||
|
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="">Tümü</option>
|
||||||
|
<option value="WH-001">Ana Depo</option>
|
||||||
|
<option value="WH-002">Üretim Deposu</option>
|
||||||
|
<option value="WH-003">Satış Deposu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Movements Table */}
|
||||||
|
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-8 text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||||
|
<p className="mt-2 text-gray-500">Hareketler yükleniyor...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Tarih/Saat
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Hareket Tipi
|
||||||
|
</th>
|
||||||
|
{!materialId && (
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Malzeme
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Miktar
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Bakiye
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Depo/Lokasyon
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Lot/Seri
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Belge
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Açıklama
|
||||||
|
</th>
|
||||||
|
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Oluşturan
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{movementsWithBalance.map((movement) => {
|
||||||
|
const typeInfo = getMovementTypeInfo(movement.movementType)
|
||||||
|
const Icon = typeInfo.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={movement.id} className="hover:bg-gray-50 text-xs">
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FaCalendar className="h-4 w-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{new Date(movement.movementDate).toLocaleDateString('tr-TR')}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{new Date(movement.movementDate).toLocaleTimeString('tr-TR')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${typeInfo.color}`}
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3 mr-1" />
|
||||||
|
{typeInfo.label}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{!materialId && (
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{movement.material.code} - {movement.material.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium ${
|
||||||
|
movement.quantity > 0 ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{movement.quantity > 0 ? '+' : ''}
|
||||||
|
{movement.quantity}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500">{movement.unit?.code}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{movement.runningBalance} {movement.unit?.code}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{movement.toWarehouse?.code}
|
||||||
|
{movement.toZone?.zoneCode && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{movement.toZone?.zoneCode}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{movement.toLocation?.locationCode && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{movement.toLocation?.locationCode}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
{movement.lotNumber && (
|
||||||
|
<div className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
|
{movement.lotNumber}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||||
|
{movement.referenceDocument && (
|
||||||
|
<div className="text-xs text-gray-900">
|
||||||
|
<div>{movement.referenceDocument}</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{movement.referenceDocumentType}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-2 py-1.5">
|
||||||
|
<div className="text-xs text-gray-900">{movement.description}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && movements.length === 0 && (
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<FaBox className="mx-auto h-10 w-10 text-gray-400" />
|
||||||
|
<h3 className="mt-2 text-base font-medium text-gray-900">Hareket bulunamadı</h3>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
Seçilen kriterlere uygun hareket kaydı bulunmuyor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MaterialMovements
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +1,24 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from "yup";
|
import * as Yup from 'yup'
|
||||||
import { FaSave, FaTimes } from "react-icons/fa";
|
import { FaSave, FaTimes } from 'react-icons/fa'
|
||||||
import { SerialStatusEnum, MmSerialNumber, MmMaterial } from "../../../types/mm";
|
import { SerialStatusEnum, MmSerialNumber, MmMaterial } from '../../../types/mm'
|
||||||
|
|
||||||
export interface SerialFormProps {
|
export interface SerialFormProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onSave: (serial: MmSerialNumber) => void;
|
onSave: (serial: MmSerialNumber) => void
|
||||||
onUpdate?: (serial: MmSerialNumber) => void;
|
onUpdate?: (serial: MmSerialNumber) => void
|
||||||
materials: MmMaterial[];
|
materials: MmMaterial[]
|
||||||
lots?: { id: string; label: string }[];
|
lots?: { id: string; label: string }[]
|
||||||
initial?: MmSerialNumber | null;
|
initial?: MmSerialNumber | null
|
||||||
mode?: "create" | "edit" | "view";
|
mode?: 'create' | 'edit' | 'view'
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
materialId: Yup.string().required("Malzeme seçimi zorunlu"),
|
materialId: Yup.string().required('Malzeme seçimi zorunlu'),
|
||||||
serialNumber: Yup.string().required("Seri numarası zorunlu"),
|
serialNumber: Yup.string().required('Seri numarası zorunlu'),
|
||||||
});
|
})
|
||||||
|
|
||||||
const SerialForm: React.FC<SerialFormProps> = ({
|
const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|
@ -28,16 +28,16 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
materials,
|
materials,
|
||||||
lots = [],
|
lots = [],
|
||||||
initial = null,
|
initial = null,
|
||||||
mode = "create",
|
mode = 'create',
|
||||||
}) => {
|
}) => {
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
materialId: "",
|
materialId: '',
|
||||||
serialNumber: "",
|
serialNumber: '',
|
||||||
lotId: lots.length ? lots[0].id : "",
|
lotId: lots.length ? lots[0].id : '',
|
||||||
productionDate: "",
|
productionDate: '',
|
||||||
warrantyExpiryDate: "",
|
warrantyExpiryDate: '',
|
||||||
currentLocationId: "",
|
currentLocationId: '',
|
||||||
status: SerialStatusEnum.Available,
|
status: SerialStatusEnum.Available,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
|
|
@ -48,51 +48,49 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
materialId: values.materialId,
|
materialId: values.materialId,
|
||||||
serialNumber: values.serialNumber,
|
serialNumber: values.serialNumber,
|
||||||
lotId: values.lotId || undefined,
|
lotId: values.lotId || undefined,
|
||||||
productionDate: values.productionDate
|
productionDate: values.productionDate ? new Date(values.productionDate) : new Date(),
|
||||||
? new Date(values.productionDate)
|
|
||||||
: new Date(),
|
|
||||||
warrantyExpiryDate: values.warrantyExpiryDate
|
warrantyExpiryDate: values.warrantyExpiryDate
|
||||||
? new Date(values.warrantyExpiryDate)
|
? new Date(values.warrantyExpiryDate)
|
||||||
: undefined,
|
: undefined,
|
||||||
currentLocationId: values.currentLocationId || undefined,
|
currentLocationId: values.currentLocationId || undefined,
|
||||||
status: values.status,
|
status: values.status,
|
||||||
isActive: !!values.isActive,
|
isActive: !!values.isActive,
|
||||||
};
|
}
|
||||||
|
|
||||||
// simulate API
|
// simulate API
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300))
|
||||||
if (mode === "edit" && onUpdate) {
|
if (mode === 'edit' && onUpdate) {
|
||||||
onUpdate(newSerial);
|
onUpdate(newSerial)
|
||||||
} else {
|
} else {
|
||||||
onSave(newSerial);
|
onSave(newSerial)
|
||||||
}
|
}
|
||||||
onClose();
|
onClose()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
// sync initial values when editing/viewing
|
// sync initial values when editing/viewing
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (initial) {
|
if (initial) {
|
||||||
const src = initial;
|
const src = initial
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
materialId: src.materialId || "",
|
materialId: src.materialId || '',
|
||||||
serialNumber: src.serialNumber || "",
|
serialNumber: src.serialNumber || '',
|
||||||
lotId: src.lotId || (lots.length ? lots[0].id : ""),
|
lotId: src.lotId || (lots.length ? lots[0].id : ''),
|
||||||
productionDate: src.productionDate
|
productionDate: src.productionDate
|
||||||
? new Date(src.productionDate).toISOString().slice(0, 10)
|
? new Date(src.productionDate).toISOString().slice(0, 10)
|
||||||
: "",
|
: '',
|
||||||
warrantyExpiryDate: src.warrantyExpiryDate
|
warrantyExpiryDate: src.warrantyExpiryDate
|
||||||
? new Date(src.warrantyExpiryDate).toISOString().slice(0, 10)
|
? new Date(src.warrantyExpiryDate).toISOString().slice(0, 10)
|
||||||
: "",
|
: '',
|
||||||
currentLocationId: src.currentLocationId || "",
|
currentLocationId: src.currentLocationId || '',
|
||||||
status: src.status || SerialStatusEnum.Available,
|
status: src.status || SerialStatusEnum.Available,
|
||||||
isActive: !!src.isActive,
|
isActive: !!src.isActive,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [initial]);
|
}, [initial])
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
|
@ -102,18 +100,18 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
<div className="flex items-center justify-between p-2 border-b">
|
<div className="flex items-center justify-between p-2 border-b">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-bold text-gray-900">
|
<h2 className="text-xl font-bold text-gray-900">
|
||||||
{mode === "create"
|
{mode === 'create'
|
||||||
? "Yeni Seri Kaydı"
|
? 'Yeni Seri Kaydı'
|
||||||
: mode === "edit"
|
: mode === 'edit'
|
||||||
? "Seri Düzenle"
|
? 'Seri Düzenle'
|
||||||
: "Seri Detayı"}
|
: 'Seri Detayı'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{mode === "create"
|
{mode === 'create'
|
||||||
? "Seri numarası girin"
|
? 'Seri numarası girin'
|
||||||
: mode === "edit"
|
: mode === 'edit'
|
||||||
? "Mevcut seri bilgisini güncelleyin"
|
? 'Mevcut seri bilgisini güncelleyin'
|
||||||
: "Seri bilgileri (sadece gösterim)"}
|
: 'Seri bilgileri (sadece gösterim)'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
@ -124,7 +122,7 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
>
|
>
|
||||||
<FaTimes className="inline mr-1 h-3 w-3" /> Kapat
|
<FaTimes className="inline mr-1 h-3 w-3" /> Kapat
|
||||||
</button>
|
</button>
|
||||||
{mode !== "view" && (
|
{mode !== 'view' && (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
|
className="px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
|
||||||
|
|
@ -137,12 +135,10 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 p-2 gap-x-3 gap-y-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 p-2 gap-x-3 gap-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Malzeme *</label>
|
||||||
Malzeme *
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("materialId")}
|
{...formik.getFieldProps('materialId')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Seçiniz...</option>
|
<option value="">Seçiniz...</option>
|
||||||
|
|
@ -155,24 +151,20 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Seri Numarası *</label>
|
||||||
Seri Numarası *
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...formik.getFieldProps("serialNumber")}
|
{...formik.getFieldProps('serialNumber')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Lot</label>
|
||||||
Lot
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("lotId")}
|
{...formik.getFieldProps('lotId')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Seçiniz...</option>
|
<option value="">Seçiniz...</option>
|
||||||
|
|
@ -185,36 +177,30 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Üretim Tarihi</label>
|
||||||
Üretim Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
{...formik.getFieldProps("productionDate")}
|
{...formik.getFieldProps('productionDate')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Garanti Bitiş</label>
|
||||||
Garanti Bitiş
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
{...formik.getFieldProps("warrantyExpiryDate")}
|
{...formik.getFieldProps('warrantyExpiryDate')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">Durum</label>
|
||||||
Durum
|
|
||||||
</label>
|
|
||||||
<select
|
<select
|
||||||
{...formik.getFieldProps("status")}
|
{...formik.getFieldProps('status')}
|
||||||
disabled={mode === "view"}
|
disabled={mode === 'view'}
|
||||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||||
>
|
>
|
||||||
<option value={SerialStatusEnum.Available}>Müsait</option>
|
<option value={SerialStatusEnum.Available}>Müsait</option>
|
||||||
|
|
@ -227,7 +213,7 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default SerialForm;
|
export default SerialForm
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom'
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
FaBuilding,
|
FaBuilding,
|
||||||
FaPlus,
|
FaPlus,
|
||||||
|
|
@ -14,45 +14,42 @@ import {
|
||||||
FaArrowUp,
|
FaArrowUp,
|
||||||
FaExclamationTriangle,
|
FaExclamationTriangle,
|
||||||
FaChartLine,
|
FaChartLine,
|
||||||
} from "react-icons/fa";
|
} from 'react-icons/fa'
|
||||||
import classNames from "classnames";
|
import classNames from 'classnames'
|
||||||
import { WarehouseTypeEnum } from "../../../types/wm";
|
import { WarehouseTypeEnum } from '../../../types/wm'
|
||||||
import { mockWarehouses } from "../../../mocks/mockWarehouses";
|
import { mockWarehouses } from '../../../mocks/mockWarehouses'
|
||||||
import {
|
import { getWarehouseTypeColor, getWarehouseTypeText } from '../../../utils/erp'
|
||||||
getWarehouseTypeColor,
|
import { Container } from '@/components/shared'
|
||||||
getWarehouseTypeText,
|
|
||||||
} from "../../../utils/erp";
|
|
||||||
|
|
||||||
const WarehouseList: React.FC = () => {
|
const WarehouseList: React.FC = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [filterType, setFilterType] = useState("all");
|
const [filterType, setFilterType] = useState('all')
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: warehouses,
|
data: warehouses,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["warehouses", searchTerm, filterType],
|
queryKey: ['warehouses', searchTerm, filterType],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
return mockWarehouses.filter((warehouse) => {
|
return mockWarehouses.filter((warehouse) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
warehouse.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
warehouse.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
warehouse.name.toLowerCase().includes(searchTerm.toLowerCase());
|
warehouse.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
const matchesType =
|
const matchesType = filterType === 'all' || warehouse.warehouseType === filterType
|
||||||
filterType === "all" || warehouse.warehouseType === filterType;
|
return matchesSearch && matchesType
|
||||||
return matchesSearch && matchesType;
|
})
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const getUtilizationColor = (utilization: number) => {
|
const getUtilizationColor = (utilization: number) => {
|
||||||
const percentage = utilization;
|
const percentage = utilization
|
||||||
if (percentage >= 90) return "text-red-600";
|
if (percentage >= 90) return 'text-red-600'
|
||||||
if (percentage >= 75) return "text-yellow-600";
|
if (percentage >= 75) return 'text-yellow-600'
|
||||||
return "text-green-600";
|
return 'text-green-600'
|
||||||
};
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -60,7 +57,7 @@ const WarehouseList: React.FC = () => {
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
<span className="ml-3 text-gray-600">Depolar yükleniyor...</span>
|
<span className="ml-3 text-gray-600">Depolar yükleniyor...</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
@ -71,378 +68,345 @@ const WarehouseList: React.FC = () => {
|
||||||
<span className="text-red-800">Depolar yüklenirken hata oluştu.</span>
|
<span className="text-red-800">Depolar yüklenirken hata oluştu.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pt-2">
|
<Container>
|
||||||
{/* Header Actions */}
|
<div className="space-y-2">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
{/* Header Actions */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||||
<div className="relative">
|
<div className="flex items-center space-x-2">
|
||||||
<FaSearch
|
<div className="relative">
|
||||||
size={16}
|
<FaSearch
|
||||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"
|
size={16}
|
||||||
/>
|
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"
|
||||||
<input
|
/>
|
||||||
type="text"
|
<input
|
||||||
placeholder="Depo kodu veya adı..."
|
type="text"
|
||||||
value={searchTerm}
|
placeholder="Depo kodu veya adı..."
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
value={searchTerm}
|
||||||
className="pl-10 pr-4 py-1.5 text-sm w-64 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
className="pl-10 pr-4 py-1.5 text-sm w-64 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
className={classNames(
|
||||||
|
'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
|
||||||
|
showFilters
|
||||||
|
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||||
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FaFilter size={14} className="mr-2" />
|
||||||
|
Filtreler
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex items-center space-x-2">
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
<button
|
||||||
className={classNames(
|
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
|
||||||
"flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors",
|
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
showFilters
|
>
|
||||||
? "border-blue-500 bg-blue-50 text-blue-700"
|
<FaDownload size={14} className="mr-2" />
|
||||||
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
Dışa Aktar
|
||||||
)}
|
</button>
|
||||||
>
|
|
||||||
<FaFilter size={14} className="mr-2" />
|
|
||||||
Filtreler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<Link
|
||||||
<button
|
to="/admin/warehouse/new"
|
||||||
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")}
|
className="flex items-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
>
|
||||||
>
|
<FaPlus size={14} className="mr-2" />
|
||||||
<FaDownload size={14} className="mr-2" />
|
Yeni Depo
|
||||||
Dışa Aktar
|
</Link>
|
||||||
</button>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/admin/warehouse/new"
|
|
||||||
className="flex items-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus size={14} className="mr-2" />
|
|
||||||
Yeni Depo
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters Panel */}
|
|
||||||
{showFilters && (
|
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
||||||
Depo Türü
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filterType}
|
|
||||||
onChange={(e) => setFilterType(e.target.value)}
|
|
||||||
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
>
|
|
||||||
<option value="all">Tümü</option>
|
|
||||||
<option value={WarehouseTypeEnum.RawMaterials}>Hammadde</option>
|
|
||||||
<option value={WarehouseTypeEnum.FinishedGoods}>Mamul</option>
|
|
||||||
<option value={WarehouseTypeEnum.WorkInProgress}>
|
|
||||||
Yarı Mamul
|
|
||||||
</option>
|
|
||||||
<option value={WarehouseTypeEnum.SpareParts}>
|
|
||||||
Yedek Parça
|
|
||||||
</option>
|
|
||||||
<option value={WarehouseTypeEnum.Quarantine}>Karantina</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-end">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setFilterType("all");
|
|
||||||
setSearchTerm("");
|
|
||||||
}}
|
|
||||||
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
Filtreleri Temizle
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Statistics Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Toplam Depo</p>
|
|
||||||
<p className="text-xl font-bold text-gray-900">
|
|
||||||
{warehouses?.length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<FaBuilding className="h-6 w-6 text-blue-500" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
{/* Filters Panel */}
|
||||||
<div className="flex items-center justify-between">
|
{showFilters && (
|
||||||
<div>
|
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||||||
<p className="text-sm font-medium text-gray-600">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
Toplam Kapasite
|
<div>
|
||||||
</p>
|
<label className="block text-xs font-medium text-gray-700 mb-1">Depo Türü</label>
|
||||||
<p className="text-xl font-bold text-green-600">
|
<select
|
||||||
{warehouses
|
value={filterType}
|
||||||
?.reduce((acc, w) => acc + w.capacity, 0)
|
onChange={(e) => setFilterType(e.target.value)}
|
||||||
.toLocaleString() || 0}
|
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
</p>
|
>
|
||||||
</div>
|
<option value="all">Tümü</option>
|
||||||
<FaBox className="h-6 w-6 text-green-500" />
|
<option value={WarehouseTypeEnum.RawMaterials}>Hammadde</option>
|
||||||
</div>
|
<option value={WarehouseTypeEnum.FinishedGoods}>Mamul</option>
|
||||||
</div>
|
<option value={WarehouseTypeEnum.WorkInProgress}>Yarı Mamul</option>
|
||||||
|
<option value={WarehouseTypeEnum.SpareParts}>Yedek Parça</option>
|
||||||
|
<option value={WarehouseTypeEnum.Quarantine}>Karantina</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
<div className="flex items-end">
|
||||||
<div className="flex items-center justify-between">
|
<button
|
||||||
<div>
|
onClick={() => {
|
||||||
<p className="text-sm font-medium text-gray-600">
|
setFilterType('all')
|
||||||
Kullanım Oranı
|
setSearchTerm('')
|
||||||
</p>
|
}}
|
||||||
<p className="text-xl font-bold text-yellow-600">
|
className="w-full px-4 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
{warehouses?.length
|
>
|
||||||
? Math.round(
|
Filtreleri Temizle
|
||||||
(warehouses.reduce(
|
</button>
|
||||||
(acc, w) => acc + w.currentUtilization,
|
</div>
|
||||||
0
|
|
||||||
) /
|
|
||||||
(warehouses.reduce((acc, w) => acc + w.capacity, 0) ||
|
|
||||||
1)) *
|
|
||||||
100
|
|
||||||
)
|
|
||||||
: 0}
|
|
||||||
%
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<FaArrowUp className="h-6 w-6 text-yellow-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Aktif Depo</p>
|
|
||||||
<p className="text-xl font-bold text-purple-600">
|
|
||||||
{warehouses?.filter((w) => w.isActive).length || 0}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<FaChartLine className="h-6 w-6 text-purple-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warehouses Table */}
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<div className="px-4 py-3 border-b border-gray-200">
|
|
||||||
<h2 className="text-xl font-bold text-gray-900">Depo Listesi</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Depo Bilgileri
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Tür / Konum
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Kapasite
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Kullanım Oranı
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Performans
|
|
||||||
</th>
|
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Durum
|
|
||||||
</th>
|
|
||||||
<th className="px-4 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">
|
|
||||||
{warehouses?.map((warehouse) => {
|
|
||||||
const utilizationPercentage = Math.round(
|
|
||||||
(warehouse.currentUtilization / warehouse.capacity) * 100
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={warehouse.id}
|
|
||||||
className="hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="flex-shrink-0 h-8 w-8">
|
|
||||||
<div className="h-8 w-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
||||||
<FaBuilding className="h-5 w-5 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<div className="text-xs font-medium text-gray-900">
|
|
||||||
{warehouse.code}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500 max-w-xs truncate">
|
|
||||||
{warehouse.name}
|
|
||||||
</div>
|
|
||||||
{warehouse.isMainWarehouse && (
|
|
||||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mt-1">
|
|
||||||
Ana Depo
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
|
||||||
getWarehouseTypeColor(warehouse.warehouseType)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{getWarehouseTypeText(warehouse.warehouseType)}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center text-xs text-gray-500">
|
|
||||||
<FaMapMarkerAlt size={14} className="mr-1" />
|
|
||||||
{warehouse.address?.city},{" "}
|
|
||||||
{warehouse.address?.country}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="text-sm font-medium text-gray-900">
|
|
||||||
{warehouse.capacity.toLocaleString()} m³
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">
|
|
||||||
Toplam Kapasite
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-gray-600">
|
|
||||||
Kullanılan:
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-gray-900">
|
|
||||||
{warehouse.currentUtilization.toLocaleString()} m³
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"h-2 rounded-full",
|
|
||||||
utilizationPercentage >= 90
|
|
||||||
? "bg-red-500"
|
|
||||||
: utilizationPercentage >= 75
|
|
||||||
? "bg-yellow-500"
|
|
||||||
: "bg-green-500"
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(utilizationPercentage, 100)}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"text-sm font-medium",
|
|
||||||
getUtilizationColor(utilizationPercentage)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
%{utilizationPercentage}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<FaChartLine
|
|
||||||
size={14}
|
|
||||||
className="text-green-500 mr-1"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-green-600">
|
|
||||||
Verimli
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500">Son 30 gün</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
|
|
||||||
warehouse.isActive
|
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-red-100 text-red-800"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{warehouse.isActive ? "Aktif" : "Pasif"}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="px-4 py-3 text-right">
|
|
||||||
<div className="flex items-center justify-end space-x-2">
|
|
||||||
<Link
|
|
||||||
to={`/admin/warehouse/warehouses/${warehouse.id}`}
|
|
||||||
className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
|
||||||
title="Detayları Görüntüle"
|
|
||||||
>
|
|
||||||
<FaEye size={16} />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to={`/admin/warehouse/edit/${warehouse.id}`}
|
|
||||||
className="p-2 text-gray-600 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
|
|
||||||
title="Düzenle"
|
|
||||||
>
|
|
||||||
<FaEdit size={16} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(!warehouses || warehouses.length === 0) && (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<FaBuilding className="mx-auto h-12 w-12 text-gray-400" />
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
|
||||||
Depo bulunamadı
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Yeni depo ekleyerek başlayın.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<Link
|
|
||||||
to="/admin/warehouse/new"
|
|
||||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
<FaPlus size={16} className="mr-2" />
|
|
||||||
Yeni Depo Ekle
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WarehouseList;
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Toplam Depo</p>
|
||||||
|
<p className="text-xl font-bold text-gray-900">{warehouses?.length || 0}</p>
|
||||||
|
</div>
|
||||||
|
<FaBuilding className="h-6 w-6 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Toplam Kapasite</p>
|
||||||
|
<p className="text-xl font-bold text-green-600">
|
||||||
|
{warehouses?.reduce((acc, w) => acc + w.capacity, 0).toLocaleString() || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FaBox className="h-6 w-6 text-green-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Kullanım Oranı</p>
|
||||||
|
<p className="text-xl font-bold text-yellow-600">
|
||||||
|
{warehouses?.length
|
||||||
|
? Math.round(
|
||||||
|
(warehouses.reduce((acc, w) => acc + w.currentUtilization, 0) /
|
||||||
|
(warehouses.reduce((acc, w) => acc + w.capacity, 0) || 1)) *
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
: 0}
|
||||||
|
%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FaArrowUp className="h-6 w-6 text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600">Aktif Depo</p>
|
||||||
|
<p className="text-xl font-bold text-purple-600">
|
||||||
|
{warehouses?.filter((w) => w.isActive).length || 0}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FaChartLine className="h-6 w-6 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warehouses Table */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<div className="px-4 py-3 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">Depo Listesi</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Depo Bilgileri
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Tür / Konum
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Kapasite
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Kullanım Oranı
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Performans
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Durum
|
||||||
|
</th>
|
||||||
|
<th className="px-4 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">
|
||||||
|
{warehouses?.map((warehouse) => {
|
||||||
|
const utilizationPercentage = Math.round(
|
||||||
|
(warehouse.currentUtilization / warehouse.capacity) * 100,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={warehouse.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0 h-8 w-8">
|
||||||
|
<div className="h-8 w-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||||
|
<FaBuilding className="h-5 w-5 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="text-xs font-medium text-gray-900">
|
||||||
|
{warehouse.code}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 max-w-xs truncate">
|
||||||
|
{warehouse.name}
|
||||||
|
</div>
|
||||||
|
{warehouse.isMainWarehouse && (
|
||||||
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mt-1">
|
||||||
|
Ana Depo
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||||
|
getWarehouseTypeColor(warehouse.warehouseType),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getWarehouseTypeText(warehouse.warehouseType)}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center text-xs text-gray-500">
|
||||||
|
<FaMapMarkerAlt size={14} className="mr-1" />
|
||||||
|
{warehouse.address?.city}, {warehouse.address?.country}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
{warehouse.capacity.toLocaleString()} m³
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">Toplam Kapasite</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-gray-600">Kullanılan:</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">
|
||||||
|
{warehouse.currentUtilization.toLocaleString()} m³
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'h-2 rounded-full',
|
||||||
|
utilizationPercentage >= 90
|
||||||
|
? 'bg-red-500'
|
||||||
|
: utilizationPercentage >= 75
|
||||||
|
? 'bg-yellow-500'
|
||||||
|
: 'bg-green-500',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(utilizationPercentage, 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'text-sm font-medium',
|
||||||
|
getUtilizationColor(utilizationPercentage),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
%{utilizationPercentage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FaChartLine size={14} className="text-green-500 mr-1" />
|
||||||
|
<span className="text-sm text-green-600">Verimli</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">Son 30 gün</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||||
|
warehouse.isActive
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{warehouse.isActive ? 'Aktif' : 'Pasif'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3 text-right">
|
||||||
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
<Link
|
||||||
|
to={`/admin/warehouse/warehouses/${warehouse.id}`}
|
||||||
|
className="p-2 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||||
|
title="Detayları Görüntüle"
|
||||||
|
>
|
||||||
|
<FaEye size={16} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to={`/admin/warehouse/edit/${warehouse.id}`}
|
||||||
|
className="p-2 text-gray-600 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
|
||||||
|
title="Düzenle"
|
||||||
|
>
|
||||||
|
<FaEdit size={16} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(!warehouses || warehouses.length === 0) && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FaBuilding className="mx-auto h-12 w-12 text-gray-400" />
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900">Depo bulunamadı</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Yeni depo ekleyerek başlayın.</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<Link
|
||||||
|
to="/admin/warehouse/new"
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<FaPlus size={16} className="mr-2" />
|
||||||
|
Yeni Depo Ekle
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WarehouseList
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue