Warehouse Management
This commit is contained in:
parent
8b88970fe2
commit
bb2e39c2c3
13 changed files with 4751 additions and 5982 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
Açıklama
|
||||
</th>
|
||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Oluşturan
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{movementsWithBalance.map((movement) => {
|
||||
const typeInfo = getMovementTypeInfo(movement.movementType);
|
||||
const Icon = typeInfo.icon;
|
||||
|
||||
return (
|
||||
<tr key={movement.id} className="hover:bg-gray-50 text-xs">
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaCalendar className="h-4 w-4 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{new Date(
|
||||
movement.movementDate
|
||||
).toLocaleDateString("tr-TR")}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{new Date(
|
||||
movement.movementDate
|
||||
).toLocaleTimeString("tr-TR")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${typeInfo.color}`}
|
||||
>
|
||||
<Icon className="h-3 w-3 mr-1" />
|
||||
{typeInfo.label}
|
||||
</div>
|
||||
</td>
|
||||
{!materialId && (
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{movement.material.code} - {movement.material.name}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
movement.quantity > 0
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}`}
|
||||
>
|
||||
{movement.quantity > 0 ? "+" : ""}
|
||||
{movement.quantity}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{movement.unit?.code}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{movement.runningBalance} {movement.unit?.code}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs text-gray-900">
|
||||
{movement.toWarehouse?.code}
|
||||
{movement.toZone?.zoneCode && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{movement.toZone?.zoneCode}
|
||||
</div>
|
||||
)}
|
||||
{movement.toLocation?.locationCode && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{movement.toLocation?.locationCode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs text-gray-900">
|
||||
{movement.lotNumber && (
|
||||
<div className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||
{movement.lotNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
{movement.referenceDocument && (
|
||||
<div className="text-xs text-gray-900">
|
||||
<div>{movement.referenceDocument}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{movement.referenceDocumentType}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-1.5">
|
||||
<div className="text-xs text-gray-900">
|
||||
{movement.description}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && movements.length === 0 && (
|
||||
<div className="text-center py-10">
|
||||
<FaBox className="mx-auto h-10 w-10 text-gray-400" />
|
||||
<h3 className="mt-2 text-base font-medium text-gray-900">
|
||||
Hareket bulunamadı
|
||||
</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Seçilen kriterlere uygun hareket kaydı bulunmuyor.
|
||||
<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">
|
||||
Açıklama
|
||||
</th>
|
||||
<th className="px-2 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Oluşturan
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{movementsWithBalance.map((movement) => {
|
||||
const typeInfo = getMovementTypeInfo(movement.movementType)
|
||||
const Icon = typeInfo.icon
|
||||
|
||||
return (
|
||||
<tr key={movement.id} className="hover:bg-gray-50 text-xs">
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaCalendar className="h-4 w-4 text-gray-400" />
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{new Date(movement.movementDate).toLocaleDateString('tr-TR')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{new Date(movement.movementDate).toLocaleTimeString('tr-TR')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${typeInfo.color}`}
|
||||
>
|
||||
<Icon className="h-3 w-3 mr-1" />
|
||||
{typeInfo.label}
|
||||
</div>
|
||||
</td>
|
||||
{!materialId && (
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{movement.material.code} - {movement.material.name}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
movement.quantity > 0 ? 'text-green-600' : 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{movement.quantity > 0 ? '+' : ''}
|
||||
{movement.quantity}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{movement.unit?.code}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{movement.runningBalance} {movement.unit?.code}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs text-gray-900">
|
||||
{movement.toWarehouse?.code}
|
||||
{movement.toZone?.zoneCode && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{movement.toZone?.zoneCode}
|
||||
</div>
|
||||
)}
|
||||
{movement.toLocation?.locationCode && (
|
||||
<div className="text-xs text-gray-500">
|
||||
{movement.toLocation?.locationCode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
<div className="text-xs text-gray-900">
|
||||
{movement.lotNumber && (
|
||||
<div className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||
{movement.lotNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-1.5 whitespace-nowrap">
|
||||
{movement.referenceDocument && (
|
||||
<div className="text-xs text-gray-900">
|
||||
<div>{movement.referenceDocument}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{movement.referenceDocumentType}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-1.5">
|
||||
<div className="text-xs text-gray-900">{movement.description}</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && movements.length === 0 && (
|
||||
<div className="text-center py-10">
|
||||
<FaBox className="mx-auto h-10 w-10 text-gray-400" />
|
||||
<h3 className="mt-2 text-base font-medium text-gray-900">Hareket bulunamadı</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Seçilen kriterlere uygun hareket kaydı bulunmuyor.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default MaterialMovements
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +1,24 @@
|
|||
import React from "react";
|
||||
import { 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
|
|
@ -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
Loading…
Reference in a new issue