erp-platform/ui/src/views/supplyChain/components/OrderManagementForm.tsx

858 lines
34 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
import React, { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
FaArrowLeft,
FaSave,
FaTimes,
FaShoppingCart,
FaCalendar,
FaDollarSign,
FaPlus,
FaTrash,
FaMapMarkerAlt,
FaUser,
} from "react-icons/fa";
import {
MmPurchaseOrder,
OrderStatusEnum,
MmPurchaseOrderItem,
} from "../../../types/mm";
import { mockMaterials } from "../../../mocks/mockMaterials";
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
import { mockPurchaseOrders } from "../../../mocks/mockPurchaseOrders";
import { Address, PaymentTerms } from "../../../types/common";
const OrderManagementForm: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const isEdit = id !== undefined && id !== "new";
const isView = window.location.pathname.includes("/view/");
// Yeni sipariş için başlangıç şablonu
const newOrderDefaults: Partial<MmPurchaseOrder> = {
orderNumber: "",
supplierId: "",
orderDate: new Date(),
deliveryDate: new Date(),
status: OrderStatusEnum.Draft,
paymentTerms: PaymentTerms.Net30,
currency: "TRY",
exchangeRate: 1,
subtotal: 0,
taxAmount: 0,
totalAmount: 0,
items: [],
deliveryAddress: {
street: "",
city: "",
state: "",
postalCode: "",
country: "Türkiye",
},
terms: "",
notes: "",
receipts: [],
};
// İlk state (isEdit vs new)
const [formData, setFormData] = useState<Partial<MmPurchaseOrder>>(() => {
if (isEdit) {
const po = mockPurchaseOrders.find((o) => o.id === id);
return { ...po };
}
return { ...newOrderDefaults };
});
// id değişirse formu güncelle
useEffect(() => {
if (isEdit) {
const po = mockPurchaseOrders.find((o) => o.id === id);
setFormData(po ? { ...po } : { ...newOrderDefaults });
} else {
setFormData({ ...newOrderDefaults });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, isEdit]);
const handleInputChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>
) => {
const { name, value, type } = e.target;
setFormData((prev) => ({
...prev,
[name]:
type === "number"
? parseFloat(value) || 0
: type === "date"
? new Date(value)
: value,
}));
};
const handleAddressChange = (field: keyof Address, value: string) => {
setFormData((prev) => ({
...prev,
deliveryAddress: {
...prev.deliveryAddress!,
[field]: value,
},
}));
};
const addOrderItem = () => {
const newItem: MmPurchaseOrderItem = {
id: `item-${Date.now()}`,
orderId: formData.id || "",
materialId: "",
description: "",
quantity: 0,
unitPrice: 0,
totalPrice: 0,
deliveryDate: new Date(),
receivedQuantity: 0,
deliveredQuantity: 0,
remainingQuantity: 0,
unit: "",
};
setFormData((prev) => ({
...prev,
items: [...(prev.items || []), newItem],
}));
};
const removeOrderItem = (index: number) => {
setFormData((prev) => ({
...prev,
items: prev.items?.filter((_, i) => i !== index) || [],
}));
calculateTotals();
};
const updateOrderItem = (
index: number,
field: keyof MmPurchaseOrderItem,
value: string | number | Date | undefined
) => {
setFormData((prev) => {
const updatedItems =
prev.items?.map((item, i) => {
if (i === index) {
const updatedItem = { ...item, [field]: value };
// Auto-calculate total amount when quantity or unit price changes
if (field === "quantity" || field === "unitPrice") {
updatedItem.totalPrice =
(updatedItem.quantity || 0) * (updatedItem.unitPrice || 0);
updatedItem.remainingQuantity =
updatedItem.quantity - (updatedItem.receivedQuantity || 0);
}
return updatedItem;
}
return item;
}) || [];
return {
...prev,
items: updatedItems,
};
});
// Recalculate totals
setTimeout(calculateTotals, 0);
};
const calculateTotals = () => {
const subtotal =
formData.items?.reduce((sum, item) => sum + (item.totalPrice || 0), 0) ||
0;
const taxAmount = subtotal * 0.18; // %18 KDV
const totalAmount = subtotal + taxAmount;
setFormData((prev) => ({
...prev,
subtotal,
taxAmount,
totalAmount,
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// TODO: Implement save logic
console.log("Saving purchase order:", formData);
navigate("/admin/supplychain/orders");
};
const handleCancel = () => {
navigate("/admin/supplychain/orders");
};
const isReadOnly = isView;
const pageTitle = isEdit
? "Satınalma Siparişini Düzenle"
: isView
? "Satınalma Siparişi Detayları"
: "Yeni Satınalma Siparişi";
return (
<div className="min-h-screen bg-gray-50 py-4">
<div className="mx-auto">
{/* Header */}
<div className="bg-white rounded-lg shadow-md p-2 mb-2">
<div className="flex items-center justify-between">
<div className="flex items-center">
<button
onClick={handleCancel}
className="mr-2 p-1.5 text-gray-400 hover:text-gray-600"
>
<FaArrowLeft />
</button>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">{pageTitle}</h2>
2025-09-15 09:31:47 +00:00
</div>
<div className="flex space-x-2">
<button
onClick={handleCancel}
className="px-3 py-1.5 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 flex items-center"
>
<FaTimes className="mr-2" />
{isView ? "Kapat" : "İptal"}
</button>
{!isView && (
<button
onClick={handleSubmit}
className="px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
>
<FaSave className="mr-2" />
Kaydet
</button>
)}
</div>
</div>
</div>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Ana İçerik */}
<div className="lg:col-span-2">
{/* Sipariş Bilgileri */}
<div className="bg-white rounded-lg shadow-md p-4 mb-4">
<h3 className="text-base font-medium text-gray-900 mb-3 flex items-center">
<FaShoppingCart className="mr-2 text-blue-600" />
Sipariş Bilgileri
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700">
Sipariş Numarası
</label>
<input
type="text"
name="orderNumber"
value={formData.orderNumber || ""}
onChange={handleInputChange}
readOnly={isReadOnly || isEdit}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="flex items-center text-sm font-medium text-gray-700">
<FaUser className="mr-1" />
Tedarikçi
</label>
<select
name="supplierId"
value={formData.supplierId || ""}
onChange={handleInputChange}
disabled={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
required
>
<option value="">Tedarikçi Seçiniz</option>
{mockBusinessParties.map((supplier) => (
<option key={supplier.id} value={supplier.id}>
{supplier.name} ({supplier.code})
</option>
))}
</select>
</div>
<div>
<label className="flex items-center text-sm font-medium text-gray-700">
<FaCalendar className="mr-1" />
Sipariş Tarihi
</label>
<input
type="date"
name="orderDate"
value={
formData.orderDate
? new Date(formData.orderDate)
.toISOString()
.split("T")[0]
: ""
}
onChange={handleInputChange}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="flex items-center text-sm font-medium text-gray-700">
<FaCalendar className="mr-1" />
Teslimat Tarihi
</label>
<input
type="date"
name="deliveryDate"
value={
formData.deliveryDate
? new Date(formData.deliveryDate)
.toISOString()
.split("T")[0]
: ""
}
onChange={handleInputChange}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Ödeme Koşulları
</label>
<select
name="paymentTerms"
value={formData.paymentTerms || PaymentTerms.Net30}
onChange={handleInputChange}
disabled={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value={PaymentTerms.Net15}>15 Gün Vadeli</option>
<option value={PaymentTerms.Net30}>30 Gün Vadeli</option>
<option value={PaymentTerms.Net45}>45 Gün Vadeli</option>
<option value={PaymentTerms.Net60}>60 Gün Vadeli</option>
<option value={PaymentTerms.Net90}>90 Gün Vadeli</option>
<option value={PaymentTerms.COD}>Kapıda Ödeme</option>
<option value={PaymentTerms.Prepaid}>Peşin</option>
<option value={PaymentTerms.Cash}>Nakit</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Para Birimi
</label>
<select
name="currency"
value={formData.currency || "TRY"}
onChange={handleInputChange}
disabled={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="TRY">TRY</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">
Şartlar ve Koşullar
</label>
<textarea
name="terms"
value={formData.terms || ""}
onChange={handleInputChange}
readOnly={isReadOnly}
rows={2}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">
Notlar
</label>
<textarea
name="notes"
value={formData.notes || ""}
onChange={handleInputChange}
readOnly={isReadOnly}
rows={1}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Teslimat Adresi */}
<div className="bg-white rounded-lg shadow-md p-4 mb-4">
<h3 className="text-base font-medium text-gray-900 mb-3 flex items-center">
<FaMapMarkerAlt className="mr-2 text-green-600" />
Teslimat Adresi
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700">
Adres
</label>
<input
type="text"
value={formData.deliveryAddress?.street || ""}
onChange={(e) =>
handleAddressChange("street", e.target.value)
}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Şehir
</label>
<input
type="text"
value={formData.deliveryAddress?.city || ""}
onChange={(e) =>
handleAddressChange("city", e.target.value)
}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
İl/Bölge
</label>
<input
type="text"
value={formData.deliveryAddress?.state || ""}
onChange={(e) =>
handleAddressChange("state", e.target.value)
}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Posta Kodu
</label>
<input
type="text"
value={formData.deliveryAddress?.postalCode || ""}
onChange={(e) =>
handleAddressChange("postalCode", e.target.value)
}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Ülke
</label>
<input
type="text"
value={formData.deliveryAddress?.country || ""}
onChange={(e) =>
handleAddressChange("country", e.target.value)
}
readOnly={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Sipariş Kalemleri */}
<div className="bg-white rounded-lg shadow-md p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">
Sipariş Kalemleri
</h3>
{!isReadOnly && (
<button
type="button"
onClick={addOrderItem}
className="px-2.5 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
>
<FaPlus className="mr-2" />
Kalem Ekle
</button>
)}
</div>
<div className="space-y-3">
{formData.items?.map((item, index) => (
<div
key={item.id}
className="border rounded-lg p-3 bg-gray-50"
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">
Kalem {index + 1}
</span>
{!isReadOnly && (
<button
type="button"
onClick={() => removeOrderItem(index)}
className="text-red-600 hover:text-red-800"
>
<FaTrash />
</button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
<div>
<label className="block text-xs font-medium text-gray-600">
Malzeme
</label>
<select
value={item.materialId}
onChange={(e) =>
updateOrderItem(
index,
"materialId",
e.target.value
)
}
disabled={isReadOnly}
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value="">Malzeme Seçiniz</option>
{mockMaterials.map((material) => (
<option key={material.id} value={material.id}>
{material.name} ({material.code})
</option>
))}
</select>
</div>
<div className="lg:col-span-2">
<label className="block text-xs font-medium text-gray-600">
ıklama
</label>
<input
type="text"
value={item.description}
onChange={(e) =>
updateOrderItem(
index,
"description",
e.target.value
)
}
readOnly={isReadOnly}
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600">
Miktar
</label>
<input
type="number"
value={item.quantity}
onChange={(e) =>
updateOrderItem(
index,
"quantity",
parseFloat(e.target.value) || 0
)
}
readOnly={isReadOnly}
min="0"
step="0.01"
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600">
Birim Fiyat
</label>
<input
type="number"
value={item.unitPrice}
onChange={(e) =>
updateOrderItem(
index,
"unitPrice",
parseFloat(e.target.value) || 0
)
}
readOnly={isReadOnly}
min="0"
step="0.01"
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600">
Toplam Tutar
</label>
<input
type="number"
value={item.totalPrice}
readOnly
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 bg-gray-100"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600">
Teslimat Tarihi
</label>
<input
type="date"
value={
item.deliveryDate
? new Date(item.deliveryDate)
.toISOString()
.split("T")[0]
: ""
}
onChange={(e) =>
updateOrderItem(
index,
"deliveryDate",
new Date(e.target.value)
)
}
readOnly={isReadOnly}
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600">
Teslim Alınan Miktar
</label>
<input
type="number"
value={item.receivedQuantity}
onChange={(e) =>
updateOrderItem(
index,
"receivedQuantity",
parseFloat(e.target.value) || 0
)
}
readOnly={isReadOnly}
min="0"
step="0.01"
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600">
Kalan Miktar
</label>
<input
type="number"
value={item.remainingQuantity}
readOnly
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 bg-gray-100"
/>
</div>
<div className="md:col-span-2 lg:col-span-3">
<label className="block text-xs font-medium text-gray-600">
Spesifikasyonlar
</label>
<input
type="text"
value={item.specifications || ""}
onChange={(e) =>
updateOrderItem(
index,
"specifications",
e.target.value
)
}
readOnly={isReadOnly}
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="md:col-span-2 lg:col-span-3">
<label className="block text-xs font-medium text-gray-600">
Kalite Gereksinimleri
</label>
<input
type="text"
value={item.qualityRequirements || ""}
onChange={(e) =>
updateOrderItem(
index,
"qualityRequirements",
e.target.value
)
}
readOnly={isReadOnly}
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
</div>
))}
{formData.items?.length === 0 && (
<div className="text-center text-gray-500 text-sm py-6">
Henüz sipariş kalemi eklenmedi
</div>
)}
</div>
</div>
</div>
{/* Yan Panel */}
<div className="space-y-4">
{/* Durum ve Tutar */}
<div className="bg-white rounded-lg shadow-md p-4">
<h3 className="text-lg font-medium text-gray-900 mb-3">
Durum Bilgileri
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Durum
</label>
<select
name="status"
value={formData.status || OrderStatusEnum.Draft}
onChange={handleInputChange}
disabled={isReadOnly}
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
>
<option value={OrderStatusEnum.Draft}>Taslak</option>
<option value={OrderStatusEnum.Sent}>Gönderildi</option>
<option value={OrderStatusEnum.Confirmed}>
Onaylandı
</option>
<option value={OrderStatusEnum.PartiallyReceived}>
Kısmi Teslim
</option>
<option value={OrderStatusEnum.Received}>
Teslim Alındı
</option>
<option value={OrderStatusEnum.Invoiced}>
Faturalandı
</option>
<option value={OrderStatusEnum.Closed}>Kapatıldı</option>
<option value={OrderStatusEnum.Cancelled}>
İptal Edildi
</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Döviz Kuru
</label>
<input
type="number"
name="exchangeRate"
value={formData.exchangeRate || 1}
onChange={handleInputChange}
readOnly={isReadOnly}
min="0"
step="0.0001"
className="mt-1 block w-full border border-gray-300 rounded-md px-2.5 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Tutar Özeti */}
<div className="bg-white rounded-lg shadow-md p-4">
<h3 className="text-base font-medium text-gray-900 mb-3 flex items-center">
<FaDollarSign className="mr-2 text-green-600" />
Tutar Özeti
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm text-gray-600">Ara Toplam:</span>
<span className="text-sm font-medium">
{formData.subtotal?.toLocaleString()} {formData.currency}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-600">KDV (%18):</span>
<span className="text-sm font-medium">
{formData.taxAmount?.toLocaleString()} {formData.currency}
</span>
</div>
<div className="border-t pt-2">
<div className="flex justify-between">
<span className="text-lg font-medium text-gray-900">
Genel Toplam:
</span>
<span className="text-lg font-bold text-green-600">
{formData.totalAmount?.toLocaleString()}{" "}
{formData.currency}
</span>
</div>
</div>
</div>
</div>
{/* Sipariş Geçmişi (sadece görüntüleme) */}
{isView && formData.receipts && formData.receipts.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-4">
<h3 className="text-base font-medium text-gray-900 mb-3">
Teslimat Geçmişi
</h3>
<div className="space-y-2">
{formData.receipts.map((receipt) => (
<div
key={receipt.id}
className="border-l-4 border-green-500 pl-4"
>
<div className="text-sm font-medium text-gray-900">
{receipt.receiptNumber}
</div>
<div className="text-xs text-gray-500">
{new Date(receipt.receiptDate).toLocaleDateString(
"tr-TR"
)}
</div>
<div className="text-xs text-gray-600">
Durumu: {receipt.status}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</form>
</div>
</div>
);
};
export default OrderManagementForm;