Warehouse Management

This commit is contained in:
Sedat Öztürk 2025-09-16 00:11:40 +03:00
parent 8b88970fe2
commit bb2e39c2c3
13 changed files with 4751 additions and 5982 deletions

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState } from 'react'
import {
FaBox,
FaMapMarkerAlt,
@ -8,521 +8,474 @@ import {
FaEye,
FaEdit,
FaPlus,
} from "react-icons/fa";
} from 'react-icons/fa'
// using modals for create actions; no navigation needed here
import {
MmLotNumber,
MmSerialNumber,
QualityStatusEnum,
SerialStatusEnum,
} from "../../../types/mm";
import LotForm from "./LotForm";
import SerialForm from "./SerialForm";
import { mockLotNumbers } from "../../../mocks/mockLotNumbers";
import { mockSerialNumbers } from "../../../mocks/mockSerialNumbers";
import { mockMaterials } from "../../../mocks/mockMaterials";
import { mockUnits } from "../../../mocks/mockUnits";
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
import Widget from "../../../components/common/Widget";
import { PartyType } from "../../../types/common";
import { getQualityStatusInfo, getSerialStatusInfo } from "../../../utils/erp";
import { MmLotNumber, MmSerialNumber, QualityStatusEnum, SerialStatusEnum } from '../../../types/mm'
import LotForm from './LotForm'
import SerialForm from './SerialForm'
import { mockLotNumbers } from '../../../mocks/mockLotNumbers'
import { mockSerialNumbers } from '../../../mocks/mockSerialNumbers'
import { mockMaterials } from '../../../mocks/mockMaterials'
import { mockUnits } from '../../../mocks/mockUnits'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import Widget from '../../../components/common/Widget'
import { PartyType } from '../../../types/common'
import { getQualityStatusInfo, getSerialStatusInfo } from '../../../utils/erp'
import { Container } from '@/components/shared'
const InventoryTracking: React.FC = () => {
const [activeTab, setActiveTab] = useState<"lots" | "serials">("lots");
const [searchTerm, setSearchTerm] = useState("");
const [showCreateMenu, setShowCreateMenu] = useState(false);
const [activeTab, setActiveTab] = useState<'lots' | 'serials'>('lots')
const [searchTerm, setSearchTerm] = useState('')
const [showCreateMenu, setShowCreateMenu] = useState(false)
// Mock data için
const [lotNumbers, setLotNumbers] = useState<MmLotNumber[]>(mockLotNumbers);
const [lotNumbers, setLotNumbers] = useState<MmLotNumber[]>(mockLotNumbers)
const [serialNumbers, setSerialNumbers] =
useState<MmSerialNumber[]>(mockSerialNumbers);
const [serialNumbers, setSerialNumbers] = useState<MmSerialNumber[]>(mockSerialNumbers)
// Modal control state
const [openLotModal, setOpenLotModal] = useState(false);
const [openSerialModal, setOpenSerialModal] = useState(false);
const [currentLot, setCurrentLot] = useState<MmLotNumber | null>(null);
const [currentSerial, setCurrentSerial] = useState<MmSerialNumber | null>(
null
);
const [openLotModal, setOpenLotModal] = useState(false)
const [openSerialModal, setOpenSerialModal] = useState(false)
const [currentLot, setCurrentLot] = useState<MmLotNumber | null>(null)
const [currentSerial, setCurrentSerial] = useState<MmSerialNumber | null>(null)
const filteredLots = lotNumbers.filter(
(lot) =>
lot.lotNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
lot.material?.name.toLowerCase().includes(searchTerm.toLowerCase())
);
lot.material?.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
const filteredSerials = serialNumbers.filter(
(serial) =>
serial.serialNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
serial.material?.name.toLowerCase().includes(searchTerm.toLowerCase())
);
serial.material?.name.toLowerCase().includes(searchTerm.toLowerCase()),
)
return (
<div className="space-y-3 py-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Envanter Takibi</h2>
<p className="text-gray-600">
Lot ve seri numarası takiplerini yönetin
</p>
</div>
<div className="relative">
<button
onClick={() =>
setShowCreateMenu((s) => !s)
} /* Kontrol küçültüldü */
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"
>
<FaPlus className="h-4 w-4" />
<span>Yeni Kayıt</span>
</button>
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Envanter Takibi</h2>
<p className="text-gray-600">Lot ve seri numarası takiplerini yönetin</p>
</div>
<div className="relative">
<button
onClick={() => setShowCreateMenu((s) => !s)} /* Kontrol küçültüldü */
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"
>
<FaPlus className="h-4 w-4" />
<span>Yeni Kayıt</span>
</button>
{/* only modal-based create menu (route-based entries removed) */}
{showCreateMenu && (
<div className="absolute right-0 mt-2 w-44 bg-white border rounded shadow-lg z-10">
<button
onClick={() => {
setOpenLotModal(true);
setShowCreateMenu(false);
}}
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
>
Yeni Lot
</button>
<button
onClick={() => {
setOpenSerialModal(true);
setShowCreateMenu(false);
}}
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
>
Yeni Seri
</button>
</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
{/* only modal-based create menu (route-based entries removed) */}
{showCreateMenu && (
<div className="absolute right-0 mt-2 w-44 bg-white border rounded shadow-lg z-10">
<button
onClick={() => {
setOpenLotModal(true)
setShowCreateMenu(false)
}}
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
>
Yeni Lot
</button>
<button
onClick={() => {
setOpenSerialModal(true)
setShowCreateMenu(false)
}}
className="w-full text-left px-2.5 py-1 text-sm hover:bg-gray-50"
>
Yeni Seri
</button>
</div>
)}
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>
</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;
{/* 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" />
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))
)
<Widget
title="Onaylı Lot"
value={
lotNumbers.filter((lot) => lot.qualityStatus === QualityStatusEnum.Approved).length
}
materials={mockMaterials}
lots={lotNumbers.map((l) => ({ id: l.id, label: l.lotNumber }))}
initial={currentSerial}
mode={currentSerial ? "edit" : "create"}
color="green"
icon="FaBox"
/>
{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>
);
};
<Widget
title="Toplam Seri"
value={serialNumbers.length}
color="purple"
icon="FaHashtag"
/>
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

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState } from 'react'
import {
FaSearch,
FaMapMarkerAlt,
@ -7,45 +7,43 @@ import {
FaEye,
FaTh,
FaList,
} from "react-icons/fa";
import { mockWarehouses } from "../../../mocks/mockWarehouses";
import { mockLocations } from "../../../mocks/mockLocations";
import { mockStockItems } from "../../../mocks/mockStockItems";
import { getStockStatusColor, getStockStatusText } from "../../../utils/erp";
} from 'react-icons/fa'
import { mockWarehouses } from '../../../mocks/mockWarehouses'
import { mockLocations } from '../../../mocks/mockLocations'
import { mockStockItems } from '../../../mocks/mockStockItems'
import { getStockStatusColor, getStockStatusText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const LocationTracking: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [selectedWarehouse, setSelectedWarehouse] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<string>("");
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [searchTerm, setSearchTerm] = useState('')
const [selectedWarehouse, setSelectedWarehouse] = useState<string>('')
const [selectedLocation, setSelectedLocation] = useState<string>('')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
const getLocationUtilization = (locationId: string) => {
const location = mockLocations.find((l) => l.id === locationId);
if (!location) return 0;
return (location.currentStock / location.capacity) * 100;
};
const location = mockLocations.find((l) => l.id === locationId)
if (!location) return 0
return (location.currentStock / location.capacity) * 100
}
const getLocationStockItems = (locationId: string) => {
return mockStockItems.filter((item) => item.locationId === locationId);
};
return mockStockItems.filter((item) => item.locationId === locationId)
}
const filteredLocations = mockLocations.filter((location) => {
const matchesSearch =
location.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
location.locationCode.toLowerCase().includes(searchTerm.toLowerCase());
const matchesWarehouse =
selectedWarehouse === "" || location.warehouseId === selectedWarehouse;
return matchesSearch && matchesWarehouse;
});
location.locationCode.toLowerCase().includes(searchTerm.toLowerCase())
const matchesWarehouse = selectedWarehouse === '' || location.warehouseId === selectedWarehouse
return matchesSearch && matchesWarehouse
})
const GridView = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredLocations.map((location) => {
const warehouse = mockWarehouses.find(
(w) => w.id === location.warehouseId
);
const locationStockItems = getLocationStockItems(location.id);
const utilization = getLocationUtilization(location.id);
const warehouse = mockWarehouses.find((w) => w.id === location.warehouseId)
const locationStockItems = getLocationStockItems(location.id)
const utilization = getLocationUtilization(location.id)
return (
<div
@ -59,9 +57,7 @@ const LocationTracking: React.FC = () => {
</div>
<div>
<h3 className="font-medium text-gray-900">{location.name}</h3>
<p className="text-sm text-gray-500">
{location.locationCode}
</p>
<p className="text-sm text-gray-500">{location.locationCode}</p>
</div>
</div>
<button
@ -84,18 +80,16 @@ const LocationTracking: React.FC = () => {
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Doluluk Oranı</span>
<span className="font-medium">
%{Math.round(utilization)}
</span>
<span className="font-medium">%{Math.round(utilization)}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${
utilization > 90
? "bg-red-500"
? 'bg-red-500'
: utilization > 70
? "bg-yellow-500"
: "bg-green-500"
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{ width: `${utilization}%` }}
/>
@ -108,29 +102,22 @@ const LocationTracking: React.FC = () => {
{/* Stock Items Summary */}
<div className="bg-gray-50 rounded-lg p-2">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-gray-900">
Malzemeler
</h4>
<span className="text-xs text-gray-500">
{locationStockItems.length} çeşit
</span>
<h4 className="text-sm font-medium text-gray-900">Malzemeler</h4>
<span className="text-xs text-gray-500">{locationStockItems.length} çeşit</span>
</div>
{locationStockItems.length > 0 ? (
<div className="space-y-1">
{locationStockItems.slice(0, 3).map((item) => (
<div
key={item.id}
className="flex justify-between items-center text-xs"
>
<div key={item.id} className="flex justify-between items-center text-xs">
<span className="text-gray-700 truncate">
{item.material?.code || "N/A"}
{item.material?.code || 'N/A'}
</span>
<div className="flex items-center gap-2">
<span className="font-medium">{item.quantity}</span>
<span
className={`px-1.5 py-0.5 rounded-full text-xs ${getStockStatusColor(
item.status
item.status,
)}`}
>
{getStockStatusText(item.status)}
@ -145,29 +132,23 @@ const LocationTracking: React.FC = () => {
)}
</div>
) : (
<div className="text-xs text-gray-500 text-center py-2">
Malzeme bulunmuyor
</div>
<div className="text-xs text-gray-500 text-center py-2">Malzeme bulunmuyor</div>
)}
</div>
{/* Restrictions */}
{location.restrictions && location.restrictions.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-1">
Kısıtlamalar
</h4>
<h4 className="text-sm font-medium text-gray-900 mb-1">Kısıtlamalar</h4>
<div className="flex flex-wrap gap-1">
{location.restrictions
.slice(0, 2)
.map((restriction, index) => (
<span
key={index}
className="inline-flex px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded"
>
{restriction}
</span>
))}
{location.restrictions.slice(0, 2).map((restriction, index) => (
<span
key={index}
className="inline-flex px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded"
>
{restriction}
</span>
))}
{location.restrictions.length > 2 && (
<span className="inline-flex px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
+{location.restrictions.length - 2}
@ -193,23 +174,21 @@ const LocationTracking: React.FC = () => {
)}
</div>
<div className="text-xs text-gray-500">
Son hareket:{" "}
Son hareket:{' '}
{locationStockItems.length > 0
? new Date(
Math.max(
...locationStockItems.map((item) =>
item.lastMovementDate.getTime()
)
)
).toLocaleDateString("tr-TR")
: "N/A"}
...locationStockItems.map((item) => item.lastMovementDate.getTime()),
),
).toLocaleDateString('tr-TR')
: 'N/A'}
</div>
</div>
</div>
);
)
})}
</div>
);
)
const ListView = () => (
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
@ -242,11 +221,9 @@ const LocationTracking: React.FC = () => {
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredLocations.map((location) => {
const warehouse = mockWarehouses.find(
(w) => w.id === location.warehouseId
);
const locationStockItems = getLocationStockItems(location.id);
const utilization = getLocationUtilization(location.id);
const warehouse = mockWarehouses.find((w) => w.id === location.warehouseId)
const locationStockItems = getLocationStockItems(location.id)
const utilization = getLocationUtilization(location.id)
return (
<tr key={location.id} className="hover:bg-gray-50">
@ -256,22 +233,14 @@ const LocationTracking: React.FC = () => {
<FaMapMarkerAlt className="w-4 h-4 text-blue-600" />
</div>
<div>
<div className="text-sm font-medium text-gray-900">
{location.name}
</div>
<div className="text-sm text-gray-500">
{location.locationCode}
</div>
<div className="text-sm font-medium text-gray-900">{location.name}</div>
<div className="text-sm text-gray-500">{location.locationCode}</div>
</div>
</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="text-sm text-gray-900">
{warehouse?.name}
</div>
<div className="text-sm text-gray-500">
{warehouse?.code}
</div>
<div className="text-sm text-gray-900">{warehouse?.name}</div>
<div className="text-sm text-gray-500">{warehouse?.code}</div>
</td>
<td className="px-3 py-2 whitespace-nowrap">
<div className="flex items-center">
@ -279,17 +248,15 @@ const LocationTracking: React.FC = () => {
<div
className={`h-2 rounded-full ${
utilization > 90
? "bg-red-500"
? 'bg-red-500'
: utilization > 70
? "bg-yellow-500"
: "bg-green-500"
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{ width: `${utilization}%` }}
/>
</div>
<span className="text-sm text-gray-900">
%{Math.round(utilization)}
</span>
<span className="text-sm text-gray-900">%{Math.round(utilization)}</span>
</div>
<div className="text-xs text-gray-500">
{location.currentStock} / {location.capacity}
@ -302,12 +269,10 @@ const LocationTracking: React.FC = () => {
{locationStockItems.length > 0
? new Date(
Math.max(
...locationStockItems.map((item) =>
item.lastMovementDate.getTime()
)
)
).toLocaleDateString("tr-TR")
: "N/A"}
...locationStockItems.map((item) => item.lastMovementDate.getTime()),
),
).toLocaleDateString('tr-TR')
: 'N/A'}
</td>
<td className="px-3 py-2 whitespace-nowrap">
{location.isActive ? (
@ -331,44 +296,37 @@ const LocationTracking: React.FC = () => {
</button>
</td>
</tr>
);
)
})}
</tbody>
</table>
</div>
</div>
);
)
const LocationDetailModal = () => {
const location = mockLocations.find((l) => l.id === selectedLocation);
const locationStockItems = getLocationStockItems(selectedLocation);
const location = mockLocations.find((l) => l.id === selectedLocation)
const locationStockItems = getLocationStockItems(selectedLocation)
if (!selectedLocation || !location) return null;
if (!selectedLocation || !location) return null
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
onClick={() => setSelectedLocation("")}
onClick={() => setSelectedLocation('')}
/>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
<div className="bg-white px-4 pt-4 pb-4 sm:p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">
{location.name} - Detaylar
</h3>
<h3 className="text-lg font-medium text-gray-900">{location.name} - Detaylar</h3>
<button
onClick={() => setSelectedLocation("")}
onClick={() => setSelectedLocation('')}
className="text-gray-400 hover:text-gray-600"
>
<svg
className="w-6 h-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -382,9 +340,7 @@ const LocationTracking: React.FC = () => {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Location Info */}
<div className="bg-gray-50 rounded-lg p-3">
<h4 className="font-medium text-gray-900 mb-3">
Lokasyon Bilgileri
</h4>
<h4 className="font-medium text-gray-900 mb-3">Lokasyon Bilgileri</h4>
<div className="space-y-2 text-sm">
<div>
<strong>Kod:</strong> {location.locationCode}
@ -399,14 +355,12 @@ const LocationTracking: React.FC = () => {
<strong>Kapasite:</strong> {location.capacity} birim
</div>
<div>
<strong>Mevcut Stok:</strong> {location.currentStock}{" "}
birim
<strong>Mevcut Stok:</strong> {location.currentStock} birim
</div>
{location.dimensions && (
<div>
<strong>Boyutlar:</strong> {location.dimensions.length}x
{location.dimensions.width}x{location.dimensions.height}
m
{location.dimensions.width}x{location.dimensions.height}m
</div>
)}
</div>
@ -419,22 +373,15 @@ const LocationTracking: React.FC = () => {
</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{locationStockItems.map((item) => (
<div
key={item.id}
className="bg-white rounded p-3 border"
>
<div key={item.id} className="bg-white rounded p-3 border">
<div className="flex justify-between items-start mb-2">
<div>
<div className="font-medium text-sm">
{item.material?.code}
</div>
<div className="text-xs text-gray-500">
{item.material?.code}
</div>
<div className="font-medium text-sm">{item.material?.code}</div>
<div className="text-xs text-gray-500">{item.material?.code}</div>
</div>
<span
className={`px-2 py-1 rounded-full text-xs ${getStockStatusColor(
item.status
item.status,
)}`}
>
{getStockStatusText(item.status)}
@ -450,11 +397,10 @@ const LocationTracking: React.FC = () => {
<div>
Rezerve: {item.reservedQuantity} {item.unitId}
</div>
<div>Lot: {item.lotNumber || "N/A"}</div>
<div>Lot: {item.lotNumber || 'N/A'}</div>
</div>
<div className="mt-2 text-xs text-gray-500">
Son hareket:{" "}
{item.lastMovementDate.toLocaleDateString("tr-TR")}
Son hareket: {item.lastMovementDate.toLocaleDateString('tr-TR')}
</div>
</div>
))}
@ -470,78 +416,76 @@ const LocationTracking: React.FC = () => {
</div>
</div>
</div>
);
};
)
}
return (
<div className="space-y-4 pt-2">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">
Raf/Lokasyon Bazlı Takip
</h2>
<p className="text-gray-600">
Lokasyonlardaki stok durumunu takip edin
</p>
<Container>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Raf/Lokasyon Bazlı Takip</h2>
<p className="text-gray-600">Lokasyonlardaki stok durumunu takip edin</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode('grid')}
className={`p-1.5 rounded-lg ${
viewMode === 'grid'
? 'bg-blue-100 text-blue-600'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<FaTh className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded-lg ${
viewMode === 'list'
? 'bg-blue-100 text-blue-600'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<FaList className="w-4 h-4" />
</button>
</div>
</div>
<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"
}`}
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Lokasyon ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-1.5 text-sm w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<select
value={selectedWarehouse}
onChange={(e) => setSelectedWarehouse(e.target.value)}
className="px-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<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>
<option value="">Tüm Depolar</option>
{mockWarehouses.map((warehouse) => (
<option key={warehouse.id} value={warehouse.id}>
{warehouse.name} ({warehouse.code})
</option>
))}
</select>
</div>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Lokasyon ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-1.5 text-sm w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<select
value={selectedWarehouse}
onChange={(e) => setSelectedWarehouse(e.target.value)}
className="px-4 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Tüm Depolar</option>
{mockWarehouses.map((warehouse) => (
<option key={warehouse.id} value={warehouse.id}>
{warehouse.name} ({warehouse.code})
</option>
))}
</select>
{/* Content */}
{viewMode === 'grid' ? <GridView /> : <ListView />}
</div>
{/* Content */}
{viewMode === "grid" ? <GridView /> : <ListView />}
{/* Location Detail Modal */}
<LocationDetailModal />
</div>
);
};
</Container>
)
}
export default LocationTracking;
export default LocationTracking

View file

@ -1,31 +1,26 @@
import React from "react";
import { useFormik } from "formik";
import * as Yup from "yup";
import { FaSave, FaTimes } from "react-icons/fa";
import {
QualityStatusEnum,
MmLotNumber,
MmUnit,
MmMaterial,
} from "../../../types/mm";
import { BusinessParty } from "../../../types/common";
import React from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { FaSave, FaTimes } from 'react-icons/fa'
import { QualityStatusEnum, MmLotNumber, MmUnit, MmMaterial } from '../../../types/mm'
import { BusinessParty } from '../../../types/common'
const validationSchema = Yup.object({
materialId: Yup.string().required("Malzeme seçimi zorunlu"),
lotNumber: Yup.string().required("Lot numarası zorunlu"),
materialId: Yup.string().required('Malzeme seçimi zorunlu'),
lotNumber: Yup.string().required('Lot numarası zorunlu'),
quantity: Yup.number().min(0, "Miktar 0'dan büyük olmalıdır"),
});
})
interface LotFormProps {
isOpen: boolean;
onClose: () => void;
onSave: (lot: MmLotNumber) => void;
onUpdate?: (lot: MmLotNumber) => void;
materials: MmMaterial[];
suppliers?: BusinessParty[];
units?: MmUnit[];
initial?: MmLotNumber | null;
mode?: "create" | "edit" | "view";
isOpen: boolean
onClose: () => void
onSave: (lot: MmLotNumber) => void
onUpdate?: (lot: MmLotNumber) => void
materials: MmMaterial[]
suppliers?: BusinessParty[]
units?: MmUnit[]
initial?: MmLotNumber | null
mode?: 'create' | 'edit' | 'view'
}
const LotForm: React.FC<LotFormProps> = ({
@ -37,17 +32,17 @@ const LotForm: React.FC<LotFormProps> = ({
suppliers = [],
units = [],
initial = null,
mode = "create",
mode = 'create',
}) => {
const formik = useFormik({
initialValues: {
materialId: "",
lotNumber: "",
productionDate: "",
expiryDate: "",
materialId: '',
lotNumber: '',
productionDate: '',
expiryDate: '',
quantity: 0,
unitId: "KG",
supplierId: suppliers.length ? suppliers[0].id : "",
unitId: 'KG',
supplierId: suppliers.length ? suppliers[0].id : '',
qualityStatus: QualityStatusEnum.Pending,
isActive: true,
},
@ -57,52 +52,48 @@ const LotForm: React.FC<LotFormProps> = ({
id: (initial && initial.id) || Date.now().toString(),
materialId: values.materialId,
lotNumber: values.lotNumber,
productionDate: values.productionDate
? new Date(values.productionDate)
: new Date(),
productionDate: values.productionDate ? new Date(values.productionDate) : new Date(),
expiryDate: values.expiryDate ? new Date(values.expiryDate) : undefined,
quantity: Number(values.quantity),
unitId: values.unitId,
supplierId: values.supplierId || undefined,
qualityStatus: values.qualityStatus,
isActive: !!values.isActive,
};
}
// simulate API
await new Promise((r) => setTimeout(r, 300));
if (mode === "edit" && onUpdate) {
onUpdate(newLot);
await new Promise((r) => setTimeout(r, 300))
if (mode === 'edit' && onUpdate) {
onUpdate(newLot)
} else {
onSave(newLot);
onSave(newLot)
}
onClose();
onClose()
},
});
})
// sync initial values when editing/viewing
React.useEffect(() => {
if (initial) {
const src = initial;
const src = initial
formik.setValues({
materialId: src.materialId || "",
lotNumber: src.lotNumber || "",
materialId: src.materialId || '',
lotNumber: src.lotNumber || '',
productionDate: src.productionDate
? new Date(src.productionDate).toISOString().slice(0, 10)
: "",
expiryDate: src.expiryDate
? new Date(src.expiryDate).toISOString().slice(0, 10)
: "",
: '',
expiryDate: src.expiryDate ? new Date(src.expiryDate).toISOString().slice(0, 10) : '',
quantity: src.quantity || 0,
unitId: src.unitId || (units.length ? units[0].id : ""),
supplierId: src.supplierId || (suppliers.length ? suppliers[0].id : ""),
unitId: src.unitId || (units.length ? units[0].id : ''),
supplierId: src.supplierId || (suppliers.length ? suppliers[0].id : ''),
qualityStatus: src.qualityStatus || QualityStatusEnum.Pending,
isActive: !!src.isActive,
});
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initial]);
}, [initial])
if (!isOpen) return null;
if (!isOpen) return null
return (
<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>
<h2 className="text-2xl font-bold text-gray-900">
{mode === "create"
? "Yeni Lot Kaydı"
: mode === "edit"
? "Lot Düzenle"
: "Lot Detayı"}
{mode === 'create'
? 'Yeni Lot Kaydı'
: mode === 'edit'
? 'Lot Düzenle'
: 'Lot Detayı'}
</h2>
<p className="text-gray-600">
{mode === "create"
? "Lot bilgilerini girin"
: mode === "edit"
? "Mevcut lot bilgilerini güncelleyin"
: "Lot bilgileri (sadece gösterim)"}
{mode === 'create'
? 'Lot bilgilerini girin'
: mode === 'edit'
? 'Mevcut lot bilgilerini güncelleyin'
: 'Lot bilgileri (sadece gösterim)'}
</p>
</div>
<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
</button>
{mode !== "view" && (
{mode !== 'view' && (
<button
type="submit"
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>
<label className="block text-sm font-medium text-gray-700">
Malzeme *
</label>
<label className="block text-sm font-medium text-gray-700">Malzeme *</label>
<select
{...formik.getFieldProps("materialId")}
disabled={mode === "view"}
{...formik.getFieldProps('materialId')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
>
<option value="">Seçiniz...</option>
@ -165,61 +154,51 @@ const LotForm: React.FC<LotFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Lot Numarası *
</label>
<label className="block text-sm font-medium text-gray-700">Lot Numarası *</label>
<input
type="text"
{...formik.getFieldProps("lotNumber")}
disabled={mode === "view"}
{...formik.getFieldProps('lotNumber')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Üretim Tarihi
</label>
<label className="block text-sm font-medium text-gray-700">Üretim Tarihi</label>
<input
type="date"
{...formik.getFieldProps("productionDate")}
disabled={mode === "view"}
{...formik.getFieldProps('productionDate')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Son Kullanma Tarihi
</label>
<label className="block text-sm font-medium text-gray-700">Son Kullanma Tarihi</label>
<input
type="date"
{...formik.getFieldProps("expiryDate")}
disabled={mode === "view"}
{...formik.getFieldProps('expiryDate')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Miktar
</label>
<label className="block text-sm font-medium text-gray-700">Miktar</label>
<input
type="number"
step="0.01"
{...formik.getFieldProps("quantity")}
disabled={mode === "view"}
{...formik.getFieldProps('quantity')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Birim
</label>
<label className="block text-sm font-medium text-gray-700">Birim</label>
<select
{...formik.getFieldProps("unitId")}
disabled={mode === "view"}
{...formik.getFieldProps('unitId')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
>
{units.map((u) => (
@ -231,12 +210,10 @@ const LotForm: React.FC<LotFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Tedarikçi
</label>
<label className="block text-sm font-medium text-gray-700">Tedarikçi</label>
<select
{...formik.getFieldProps("supplierId")}
disabled={mode === "view"}
{...formik.getFieldProps('supplierId')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
>
<option value="">Seçiniz...</option>
@ -249,11 +226,9 @@ const LotForm: React.FC<LotFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Kalite Durumu
</label>
<label className="block text-sm font-medium text-gray-700">Kalite Durumu</label>
<select
{...formik.getFieldProps("qualityStatus")}
{...formik.getFieldProps('qualityStatus')}
className="w-full border rounded-md px-2 py-1 text-sm"
>
<option value={QualityStatusEnum.Pending}>Beklemede</option>
@ -266,7 +241,7 @@ const LotForm: React.FC<LotFormProps> = ({
</form>
</div>
</div>
);
};
)
}
export default LotForm;
export default LotForm

View file

@ -1,382 +1,346 @@
import React, { useState, useEffect, useCallback } from "react";
import { useParams } from "react-router-dom";
import { FaDownload, FaCalendar, FaBox } from "react-icons/fa";
import { MmStockMovement, MovementTypeEnum } from "../../../types/mm";
import { FaRepeat } from "react-icons/fa6";
import { getMovementTypeInfo } from "../../../utils/erp";
import { mockStockMovements } from "../../../mocks/mockStockMovements";
import Widget, { colorType } from "../../../components/common/Widget";
import React, { useState, useEffect, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import { FaDownload, FaCalendar, FaBox } from 'react-icons/fa'
import { MmStockMovement, MovementTypeEnum } from '../../../types/mm'
import { FaRepeat } from 'react-icons/fa6'
import { getMovementTypeInfo } from '../../../utils/erp'
import { mockStockMovements } from '../../../mocks/mockStockMovements'
import Widget, { colorType } from '../../../components/common/Widget'
import { Container } from '@/components/shared'
const MaterialMovements: React.FC<{ materialId?: string }> = ({
materialId,
}) => {
const params = useParams<Record<string, string | undefined>>();
const routeMaterialId =
materialId || params.materialId || params.id || undefined;
const [movements, setMovements] = useState<MmStockMovement[]>([]);
const [loading, setLoading] = useState(false);
const MaterialMovements: React.FC<{ materialId?: string }> = ({ materialId }) => {
const params = useParams<Record<string, string | undefined>>()
const routeMaterialId = materialId || params.materialId || params.id || undefined
const [movements, setMovements] = useState<MmStockMovement[]>([])
const [loading, setLoading] = useState(false)
const [filters, setFilters] = useState({
startDate: "",
endDate: "",
movementType: "",
warehouseId: "",
});
startDate: '',
endDate: '',
movementType: '',
warehouseId: '',
})
const loadMovements = useCallback(() => {
setLoading(true);
setLoading(true)
// Simulate API call
setTimeout(() => {
let filteredMovements = mockStockMovements;
let filteredMovements = mockStockMovements
const activeMaterialId = routeMaterialId;
const activeMaterialId = routeMaterialId
if (activeMaterialId) {
filteredMovements = filteredMovements.filter(
(m) => m.materialId === activeMaterialId
);
filteredMovements = filteredMovements.filter((m) => m.materialId === activeMaterialId)
}
if (filters.movementType) {
filteredMovements = filteredMovements.filter(
(m) =>
m.movementType ===
(filters.movementType as unknown as MovementTypeEnum)
);
(m) => m.movementType === (filters.movementType as unknown as MovementTypeEnum),
)
}
setMovements(filteredMovements);
setLoading(false);
}, 1000);
}, [routeMaterialId, filters]);
setMovements(filteredMovements)
setLoading(false)
}, 1000)
}, [routeMaterialId, filters])
useEffect(() => {
loadMovements();
}, [loadMovements]);
loadMovements()
}, [loadMovements])
const calculateRunningBalance = () => {
let balance = 0;
let balance = 0
return movements
.sort(
(a, b) =>
new Date(a.movementDate).getTime() -
new Date(b.movementDate).getTime()
)
.sort((a, b) => new Date(a.movementDate).getTime() - new Date(b.movementDate).getTime())
.map((movement) => {
balance += movement.quantity;
return { ...movement, runningBalance: balance };
});
};
balance += movement.quantity
return { ...movement, runningBalance: balance }
})
}
const movementsWithBalance = calculateRunningBalance();
const movementsWithBalance = calculateRunningBalance()
return (
<div className="space-y-3 py-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">
{materialId ? "Stok Hareketleri" : "Tüm Stok Hareketleri"}
</h2>
<p className="text-gray-600">
{materialId
? `${materialId} kodlu malzeme için`
: "Tüm malzemeler için"}{" "}
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">
ı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.
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">
{materialId ? 'Stok Hareketleri' : 'Tüm Stok Hareketleri'}
</h2>
<p className="text-gray-600">
{materialId ? `${materialId} kodlu malzeme için` : 'Tüm malzemeler için'} stok
hareketlerini görüntüleyin
</p>
</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">
ı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

View file

@ -1,24 +1,24 @@
import React from "react";
import { useFormik } from "formik";
import * as Yup from "yup";
import { FaSave, FaTimes } from "react-icons/fa";
import { SerialStatusEnum, MmSerialNumber, MmMaterial } from "../../../types/mm";
import React from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { FaSave, FaTimes } from 'react-icons/fa'
import { SerialStatusEnum, MmSerialNumber, MmMaterial } from '../../../types/mm'
export interface SerialFormProps {
isOpen: boolean;
onClose: () => void;
onSave: (serial: MmSerialNumber) => void;
onUpdate?: (serial: MmSerialNumber) => void;
materials: MmMaterial[];
lots?: { id: string; label: string }[];
initial?: MmSerialNumber | null;
mode?: "create" | "edit" | "view";
isOpen: boolean
onClose: () => void
onSave: (serial: MmSerialNumber) => void
onUpdate?: (serial: MmSerialNumber) => void
materials: MmMaterial[]
lots?: { id: string; label: string }[]
initial?: MmSerialNumber | null
mode?: 'create' | 'edit' | 'view'
}
const validationSchema = Yup.object({
materialId: Yup.string().required("Malzeme seçimi zorunlu"),
serialNumber: Yup.string().required("Seri numarası zorunlu"),
});
materialId: Yup.string().required('Malzeme seçimi zorunlu'),
serialNumber: Yup.string().required('Seri numarası zorunlu'),
})
const SerialForm: React.FC<SerialFormProps> = ({
isOpen,
@ -28,16 +28,16 @@ const SerialForm: React.FC<SerialFormProps> = ({
materials,
lots = [],
initial = null,
mode = "create",
mode = 'create',
}) => {
const formik = useFormik({
initialValues: {
materialId: "",
serialNumber: "",
lotId: lots.length ? lots[0].id : "",
productionDate: "",
warrantyExpiryDate: "",
currentLocationId: "",
materialId: '',
serialNumber: '',
lotId: lots.length ? lots[0].id : '',
productionDate: '',
warrantyExpiryDate: '',
currentLocationId: '',
status: SerialStatusEnum.Available,
isActive: true,
},
@ -48,51 +48,49 @@ const SerialForm: React.FC<SerialFormProps> = ({
materialId: values.materialId,
serialNumber: values.serialNumber,
lotId: values.lotId || undefined,
productionDate: values.productionDate
? new Date(values.productionDate)
: new Date(),
productionDate: values.productionDate ? new Date(values.productionDate) : new Date(),
warrantyExpiryDate: values.warrantyExpiryDate
? new Date(values.warrantyExpiryDate)
: undefined,
currentLocationId: values.currentLocationId || undefined,
status: values.status,
isActive: !!values.isActive,
};
}
// simulate API
await new Promise((r) => setTimeout(r, 300));
if (mode === "edit" && onUpdate) {
onUpdate(newSerial);
await new Promise((r) => setTimeout(r, 300))
if (mode === 'edit' && onUpdate) {
onUpdate(newSerial)
} else {
onSave(newSerial);
onSave(newSerial)
}
onClose();
onClose()
},
});
})
// sync initial values when editing/viewing
React.useEffect(() => {
if (initial) {
const src = initial;
const src = initial
formik.setValues({
materialId: src.materialId || "",
serialNumber: src.serialNumber || "",
lotId: src.lotId || (lots.length ? lots[0].id : ""),
materialId: src.materialId || '',
serialNumber: src.serialNumber || '',
lotId: src.lotId || (lots.length ? lots[0].id : ''),
productionDate: src.productionDate
? new Date(src.productionDate).toISOString().slice(0, 10)
: "",
: '',
warrantyExpiryDate: src.warrantyExpiryDate
? new Date(src.warrantyExpiryDate).toISOString().slice(0, 10)
: "",
currentLocationId: src.currentLocationId || "",
: '',
currentLocationId: src.currentLocationId || '',
status: src.status || SerialStatusEnum.Available,
isActive: !!src.isActive,
});
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initial]);
}, [initial])
if (!isOpen) return null;
if (!isOpen) return null
return (
<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>
<h2 className="text-xl font-bold text-gray-900">
{mode === "create"
? "Yeni Seri Kaydı"
: mode === "edit"
? "Seri Düzenle"
: "Seri Detayı"}
{mode === 'create'
? 'Yeni Seri Kaydı'
: mode === 'edit'
? 'Seri Düzenle'
: 'Seri Detayı'}
</h2>
<p className="text-sm text-gray-600">
{mode === "create"
? "Seri numarası girin"
: mode === "edit"
? "Mevcut seri bilgisini güncelleyin"
: "Seri bilgileri (sadece gösterim)"}
{mode === 'create'
? 'Seri numarası girin'
: mode === 'edit'
? 'Mevcut seri bilgisini güncelleyin'
: 'Seri bilgileri (sadece gösterim)'}
</p>
</div>
<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
</button>
{mode !== "view" && (
{mode !== 'view' && (
<button
type="submit"
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>
<label className="block text-sm font-medium text-gray-700">
Malzeme *
</label>
<label className="block text-sm font-medium text-gray-700">Malzeme *</label>
<select
{...formik.getFieldProps("materialId")}
disabled={mode === "view"}
{...formik.getFieldProps('materialId')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
>
<option value="">Seçiniz...</option>
@ -155,24 +151,20 @@ const SerialForm: React.FC<SerialFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Seri Numarası *
</label>
<label className="block text-sm font-medium text-gray-700">Seri Numarası *</label>
<input
type="text"
{...formik.getFieldProps("serialNumber")}
disabled={mode === "view"}
{...formik.getFieldProps('serialNumber')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Lot
</label>
<label className="block text-sm font-medium text-gray-700">Lot</label>
<select
{...formik.getFieldProps("lotId")}
disabled={mode === "view"}
{...formik.getFieldProps('lotId')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
>
<option value="">Seçiniz...</option>
@ -185,36 +177,30 @@ const SerialForm: React.FC<SerialFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Üretim Tarihi
</label>
<label className="block text-sm font-medium text-gray-700">Üretim Tarihi</label>
<input
type="date"
{...formik.getFieldProps("productionDate")}
disabled={mode === "view"}
{...formik.getFieldProps('productionDate')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Garanti Bitiş
</label>
<label className="block text-sm font-medium text-gray-700">Garanti Bitiş</label>
<input
type="date"
{...formik.getFieldProps("warrantyExpiryDate")}
disabled={mode === "view"}
{...formik.getFieldProps('warrantyExpiryDate')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Durum
</label>
<label className="block text-sm font-medium text-gray-700">Durum</label>
<select
{...formik.getFieldProps("status")}
disabled={mode === "view"}
{...formik.getFieldProps('status')}
disabled={mode === 'view'}
className="w-full border rounded-md px-2 py-1 text-sm"
>
<option value={SerialStatusEnum.Available}>Müsait</option>
@ -227,7 +213,7 @@ const SerialForm: React.FC<SerialFormProps> = ({
</form>
</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

View file

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import {
FaBuilding,
FaPlus,
@ -14,45 +14,42 @@ import {
FaArrowUp,
FaExclamationTriangle,
FaChartLine,
} from "react-icons/fa";
import classNames from "classnames";
import { WarehouseTypeEnum } from "../../../types/wm";
import { mockWarehouses } from "../../../mocks/mockWarehouses";
import {
getWarehouseTypeColor,
getWarehouseTypeText,
} from "../../../utils/erp";
} from 'react-icons/fa'
import classNames from 'classnames'
import { WarehouseTypeEnum } from '../../../types/wm'
import { mockWarehouses } from '../../../mocks/mockWarehouses'
import { getWarehouseTypeColor, getWarehouseTypeText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const WarehouseList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [filterType, setFilterType] = useState("all");
const [showFilters, setShowFilters] = useState(false);
const [searchTerm, setSearchTerm] = useState('')
const [filterType, setFilterType] = useState('all')
const [showFilters, setShowFilters] = useState(false)
const {
data: warehouses,
isLoading,
error,
} = useQuery({
queryKey: ["warehouses", searchTerm, filterType],
queryKey: ['warehouses', searchTerm, filterType],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
await new Promise((resolve) => setTimeout(resolve, 500))
return mockWarehouses.filter((warehouse) => {
const matchesSearch =
warehouse.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
warehouse.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType =
filterType === "all" || warehouse.warehouseType === filterType;
return matchesSearch && matchesType;
});
warehouse.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesType = filterType === 'all' || warehouse.warehouseType === filterType
return matchesSearch && matchesType
})
},
});
})
const getUtilizationColor = (utilization: number) => {
const percentage = utilization;
if (percentage >= 90) return "text-red-600";
if (percentage >= 75) return "text-yellow-600";
return "text-green-600";
};
const percentage = utilization
if (percentage >= 90) return 'text-red-600'
if (percentage >= 75) return 'text-yellow-600'
return 'text-green-600'
}
if (isLoading) {
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>
<span className="ml-3 text-gray-600">Depolar yükleniyor...</span>
</div>
);
)
}
if (error) {
@ -71,378 +68,345 @@ const WarehouseList: React.FC = () => {
<span className="text-red-800">Depolar yüklenirken hata oluştu.</span>
</div>
</div>
);
)
}
return (
<div className="space-y-4 pt-2">
{/* Header Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center space-x-2">
<div className="relative">
<FaSearch
size={16}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"
/>
<input
type="text"
placeholder="Depo kodu veya adı..."
value={searchTerm}
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"
/>
<Container>
<div className="space-y-2">
{/* Header Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center space-x-2">
<div className="relative">
<FaSearch
size={16}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"
/>
<input
type="text"
placeholder="Depo kodu veya adı..."
value={searchTerm}
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>
<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 className="flex items-center space-x-2">
<button
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
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"
>
<FaDownload size={14} className="mr-2" />
Dışa Aktar
</button>
<div className="flex items-center space-x-2">
<button
onClick={() => alert("Dışa aktarma özelliği yakında eklenecek")}
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"
>
<FaDownload size={14} className="mr-2" />
Dışa Aktar
</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" />
<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>
<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>
{/* 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="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 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>
)}
</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