429 lines
13 KiB
TypeScript
429 lines
13 KiB
TypeScript
|
|
import React, { useState } from "react";
|
|||
|
|
import {
|
|||
|
|
FaCheckCircle,
|
|||
|
|
FaClock,
|
|||
|
|
FaExclamationTriangle,
|
|||
|
|
FaCalendar,
|
|||
|
|
} from "react-icons/fa";
|
|||
|
|
import {
|
|||
|
|
RecommendationTypeEnum,
|
|||
|
|
RecommendationStatusEnum,
|
|||
|
|
MrpPurchaseSuggestion,
|
|||
|
|
} from "../../../types/mrp";
|
|||
|
|
import DataTable, { Column } from "../../../components/common/DataTable";
|
|||
|
|
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
|||
|
|
import { PriorityEnum } from "../../../types/common";
|
|||
|
|
import {
|
|||
|
|
getPriorityColor,
|
|||
|
|
getPriorityText,
|
|||
|
|
getRecommendationStatusColor,
|
|||
|
|
getRecommendationStatusText,
|
|||
|
|
} from "../../../utils/erp";
|
|||
|
|
|
|||
|
|
interface PurchaseSuggestionsProps {
|
|||
|
|
purchaseSuggestions: MrpPurchaseSuggestion[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const PurchaseSuggestions: React.FC<PurchaseSuggestionsProps> = ({
|
|||
|
|
purchaseSuggestions,
|
|||
|
|
}) => {
|
|||
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|||
|
|
const [selectedStatus, setSelectedStatus] = useState<
|
|||
|
|
RecommendationStatusEnum | "all"
|
|||
|
|
>("all");
|
|||
|
|
const [selectedPriority, setSelectedPriority] = useState<
|
|||
|
|
PriorityEnum | "all"
|
|||
|
|
>("all");
|
|||
|
|
const [sortBy, setSortBy] = useState<
|
|||
|
|
"date" | "cost" | "priority" | "leadTime"
|
|||
|
|
>("date");
|
|||
|
|
|
|||
|
|
// Event handlers
|
|||
|
|
const handleCreatePurchaseRequest = (suggestion: MrpPurchaseSuggestion) => {
|
|||
|
|
console.log("Create purchase request:", suggestion);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleApprove = (id: string) => {
|
|||
|
|
console.log("Approve suggestion:", id);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleReject = (id: string) => {
|
|||
|
|
console.log("Reject suggestion:", id);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleViewDetails = (suggestion: MrpPurchaseSuggestion) => {
|
|||
|
|
console.log("View suggestion details:", suggestion);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const filteredSuggestions = purchaseSuggestions
|
|||
|
|
.filter((suggestion) => {
|
|||
|
|
if (
|
|||
|
|
searchTerm &&
|
|||
|
|
!suggestion.material?.name
|
|||
|
|
?.toLowerCase()
|
|||
|
|
.includes(searchTerm.toLowerCase()) &&
|
|||
|
|
!suggestion.recommendedAction
|
|||
|
|
.toLowerCase()
|
|||
|
|
.includes(searchTerm.toLowerCase())
|
|||
|
|
) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (selectedStatus !== "all" && suggestion.status !== selectedStatus) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (
|
|||
|
|
selectedPriority !== "all" &&
|
|||
|
|
suggestion.priority !== selectedPriority
|
|||
|
|
) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return (
|
|||
|
|
suggestion.recommendationType ===
|
|||
|
|
RecommendationTypeEnum.PurchaseRequisition
|
|||
|
|
);
|
|||
|
|
})
|
|||
|
|
.sort((a, b) => {
|
|||
|
|
switch (sortBy) {
|
|||
|
|
case "date":
|
|||
|
|
return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
|
|||
|
|
case "cost":
|
|||
|
|
return b.estimatedCost - a.estimatedCost;
|
|||
|
|
case "priority": {
|
|||
|
|
const priorityOrder = { URGENT: 4, HIGH: 3, NORMAL: 2, LOW: 1 };
|
|||
|
|
return (
|
|||
|
|
(priorityOrder[b.priority as keyof typeof priorityOrder] || 0) -
|
|||
|
|
(priorityOrder[a.priority as keyof typeof priorityOrder] || 0)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
case "leadTime":
|
|||
|
|
return a.leadTime - b.leadTime;
|
|||
|
|
default:
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const getUrgencyLevel = (suggestion: MrpPurchaseSuggestion) => {
|
|||
|
|
const today = new Date();
|
|||
|
|
const dueDate = new Date(suggestion.dueDate);
|
|||
|
|
const leadTimeDate = new Date(
|
|||
|
|
today.getTime() + suggestion.leadTime * 24 * 60 * 60 * 1000
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (leadTimeDate > dueDate) {
|
|||
|
|
return {
|
|||
|
|
level: "critical",
|
|||
|
|
color: "bg-red-100 text-red-800",
|
|||
|
|
label: "Kritik",
|
|||
|
|
icon: FaExclamationTriangle,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const daysUntilDue = Math.ceil(
|
|||
|
|
(dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (daysUntilDue <= 7)
|
|||
|
|
return {
|
|||
|
|
level: "urgent",
|
|||
|
|
color: "bg-orange-100 text-orange-800",
|
|||
|
|
label: "Acil",
|
|||
|
|
icon: FaClock,
|
|||
|
|
};
|
|||
|
|
if (daysUntilDue <= 30)
|
|||
|
|
return {
|
|||
|
|
level: "soon",
|
|||
|
|
color: "bg-yellow-100 text-yellow-800",
|
|||
|
|
label: "Yakın",
|
|||
|
|
icon: FaCalendar,
|
|||
|
|
};
|
|||
|
|
return {
|
|||
|
|
level: "normal",
|
|||
|
|
color: "bg-green-100 text-green-800",
|
|||
|
|
label: "Normal",
|
|||
|
|
icon: FaCheckCircle,
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const columns: Column<MrpPurchaseSuggestion>[] = [
|
|||
|
|
{
|
|||
|
|
key: "material",
|
|||
|
|
header: "Malzeme",
|
|||
|
|
sortable: true,
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<div>
|
|||
|
|
<div className="font-medium text-gray-900">
|
|||
|
|
{suggestion.material?.name ||
|
|||
|
|
`Material-${suggestion.materialId.substring(0, 8)}`}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-gray-500">
|
|||
|
|
{suggestion.material?.code}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "urgency",
|
|||
|
|
header: "Aciliyet",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => {
|
|||
|
|
const urgency = getUrgencyLevel(suggestion);
|
|||
|
|
const IconComponent = urgency.icon;
|
|||
|
|
return (
|
|||
|
|
<div className="flex items-center gap-1.5">
|
|||
|
|
<span
|
|||
|
|
className={`px-1.5 py-0.5 text-xs font-medium rounded-full ${urgency.color}`}
|
|||
|
|
>
|
|||
|
|
{urgency.label}
|
|||
|
|
</span>
|
|||
|
|
<IconComponent className="w-3.5 h-3.5 text-gray-400" />
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "quantities",
|
|||
|
|
header: "Miktarlar",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<div className="text-sm">
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span>İhtiyaç:</span>
|
|||
|
|
<span className="font-medium">
|
|||
|
|
{suggestion.quantity.toLocaleString()}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span>Önerilen:</span>
|
|||
|
|
<span className="font-medium text-blue-600">
|
|||
|
|
{suggestion.suggestedQuantity.toLocaleString()}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span>EOQ:</span>
|
|||
|
|
<span className="font-medium text-green-600">
|
|||
|
|
{suggestion.economicOrderQuantity.toLocaleString()}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
{suggestion.minimumOrderQuantity > 0 && (
|
|||
|
|
<div className="flex justify-between text-xs text-gray-500">
|
|||
|
|
<span>Min:</span>
|
|||
|
|
<span>{suggestion.minimumOrderQuantity.toLocaleString()}</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "supplier",
|
|||
|
|
header: "Tedarikçi",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<div>
|
|||
|
|
{suggestion.supplier ? (
|
|||
|
|
<div>
|
|||
|
|
<div className="font-medium text-gray-900">
|
|||
|
|
{
|
|||
|
|
mockBusinessParties.find(
|
|||
|
|
(a) => a.id === suggestion.supplier?.supplierId
|
|||
|
|
)?.name
|
|||
|
|
}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<span className="text-gray-400">Tedarikçi seçilmedi</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "cost",
|
|||
|
|
header: "Maliyet",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<div className="text-right">
|
|||
|
|
<div className="font-medium text-gray-900">
|
|||
|
|
₺{suggestion.estimatedCost.toLocaleString()}
|
|||
|
|
</div>
|
|||
|
|
{suggestion.supplier && (
|
|||
|
|
<div className="text-xs text-gray-500">
|
|||
|
|
Birim: ₺{suggestion.supplier.price.toLocaleString()}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "dates",
|
|||
|
|
header: "Tarihler",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<div className="text-sm">
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<FaCalendar className="w-3 h-3 text-gray-400" />
|
|||
|
|
<span>
|
|||
|
|
Vade: {new Date(suggestion.dueDate).toLocaleDateString("tr-TR")}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex items-center gap-1">
|
|||
|
|
<FaClock className="w-3 h-3 text-gray-400" />
|
|||
|
|
<span>Teslimat: {suggestion.leadTime} gün</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "priority",
|
|||
|
|
header: "Öncelik",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<span
|
|||
|
|
className={`px-1.5 py-0.5 text-xs font-medium rounded-full ${getPriorityColor(
|
|||
|
|
suggestion.priority
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getPriorityText(suggestion.priority)}
|
|||
|
|
</span>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "status",
|
|||
|
|
header: "Durum",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<span
|
|||
|
|
className={`px-1.5 py-0.5 text-xs font-medium rounded-full ${getRecommendationStatusColor(
|
|||
|
|
suggestion.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getRecommendationStatusText(suggestion.status)}
|
|||
|
|
</span>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "actions",
|
|||
|
|
header: "İşlemler",
|
|||
|
|
render: (suggestion: MrpPurchaseSuggestion) => (
|
|||
|
|
<div className="flex gap-1">
|
|||
|
|
{suggestion.status === RecommendationStatusEnum.Open && (
|
|||
|
|
<>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleCreatePurchaseRequest(suggestion)}
|
|||
|
|
className="px-1.5 py-0.5 text-xs bg-blue-50 text-blue-600 rounded hover:bg-blue-100 transition-colors"
|
|||
|
|
title="Satın Alma Talebi Oluştur"
|
|||
|
|
>
|
|||
|
|
Talep Oluştur
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleApprove(suggestion.id)}
|
|||
|
|
className="px-1.5 py-0.5 text-xs bg-green-50 text-green-600 rounded hover:bg-green-100 transition-colors"
|
|||
|
|
title="Onayla"
|
|||
|
|
>
|
|||
|
|
Onayla
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleReject(suggestion.id)}
|
|||
|
|
className="px-1.5 py-0.5 text-xs bg-red-50 text-red-600 rounded hover:bg-red-100 transition-colors"
|
|||
|
|
title="Reddet"
|
|||
|
|
>
|
|||
|
|
Reddet
|
|||
|
|
</button>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleViewDetails(suggestion)}
|
|||
|
|
className="px-1.5 py-0.5 text-xs bg-gray-50 text-gray-600 rounded hover:bg-gray-100 transition-colors"
|
|||
|
|
title="Detayları Görüntüle"
|
|||
|
|
>
|
|||
|
|
Detay
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Urgency distribution
|
|||
|
|
const urgencyDistribution = [
|
|||
|
|
{ level: "critical", count: 0, cost: 0 },
|
|||
|
|
{ level: "urgent", count: 0, cost: 0 },
|
|||
|
|
{ level: "soon", count: 0, cost: 0 },
|
|||
|
|
{ level: "normal", count: 0, cost: 0 },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
purchaseSuggestions
|
|||
|
|
.filter((s) => s.status === RecommendationStatusEnum.Open)
|
|||
|
|
.forEach((s) => {
|
|||
|
|
const urgency = getUrgencyLevel(s);
|
|||
|
|
const index = urgencyDistribution.findIndex(
|
|||
|
|
(u) => u.level === urgency.level
|
|||
|
|
);
|
|||
|
|
if (index >= 0) {
|
|||
|
|
urgencyDistribution[index].count++;
|
|||
|
|
urgencyDistribution[index].cost += s.estimatedCost;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
{/* Filters */}
|
|||
|
|
<div className="flex gap-2 items-center text-sm">
|
|||
|
|
<div className="flex-1">
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
placeholder="Malzeme adı, tedarikçi veya aksiyon ara..."
|
|||
|
|
value={searchTerm}
|
|||
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|||
|
|
className="w-full px-2 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedStatus}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setSelectedStatus(
|
|||
|
|
e.target.value as RecommendationStatusEnum | "all"
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="px-2 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Durumlar</option>
|
|||
|
|
{Object.values(RecommendationStatusEnum).map((status) => (
|
|||
|
|
<option key={status} value={status}>
|
|||
|
|
{getRecommendationStatusText(status)}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedPriority}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setSelectedPriority(e.target.value as PriorityEnum | "all")
|
|||
|
|
}
|
|||
|
|
className="px-2 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Öncelikler</option>
|
|||
|
|
{Object.values(PriorityEnum).map((priority) => (
|
|||
|
|
<option key={priority} value={priority}>
|
|||
|
|
{getPriorityText(priority)}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={sortBy}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setSortBy(
|
|||
|
|
e.target.value as "date" | "cost" | "priority" | "leadTime"
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="px-2 py-1.5 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|||
|
|
>
|
|||
|
|
<option value="date">Tarihe Göre</option>
|
|||
|
|
<option value="cost">Maliyete Göre</option>
|
|||
|
|
<option value="priority">Önceliğe Göre</option>
|
|||
|
|
<option value="leadTime">Teslimat Süresine Göre</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Data Table */}
|
|||
|
|
<div className="bg-white rounded-lg shadow-sm border">
|
|||
|
|
<DataTable data={filteredSuggestions} columns={columns} />
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default PurchaseSuggestions;
|