2025-09-15 09:31:47 +00:00
|
|
|
|
import React, { useState } from "react";
|
|
|
|
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
|
|
|
|
import {
|
|
|
|
|
|
FaArrowLeft,
|
|
|
|
|
|
FaSave,
|
|
|
|
|
|
FaTimes,
|
|
|
|
|
|
FaFileAlt,
|
|
|
|
|
|
FaCalendar,
|
|
|
|
|
|
FaDollarSign,
|
|
|
|
|
|
FaPlus,
|
|
|
|
|
|
FaTrash,
|
|
|
|
|
|
FaPaperclip,
|
|
|
|
|
|
} from "react-icons/fa";
|
|
|
|
|
|
import {
|
|
|
|
|
|
MmQuotation,
|
|
|
|
|
|
QuotationStatusEnum,
|
|
|
|
|
|
RequestTypeEnum,
|
|
|
|
|
|
MmQuotationItem,
|
|
|
|
|
|
MmAttachment,
|
|
|
|
|
|
} from "../../../types/mm";
|
|
|
|
|
|
import { mockMaterials } from "../../../mocks/mockMaterials";
|
|
|
|
|
|
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
|
|
|
|
|
|
|
|
|
|
|
const QuotationForm: React.FC = () => {
|
|
|
|
|
|
const { id } = useParams<{ id: string }>();
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const isEdit = id !== undefined && id !== "new";
|
|
|
|
|
|
const isView = window.location.pathname.includes("/view/");
|
|
|
|
|
|
|
|
|
|
|
|
const [formData, setFormData] = useState<Partial<MmQuotation>>({
|
|
|
|
|
|
quotationNumber: isEdit ? `TEK-2024-${id}` : "",
|
|
|
|
|
|
requestId: "",
|
|
|
|
|
|
requestTitle: "",
|
|
|
|
|
|
requestType: RequestTypeEnum.Material,
|
|
|
|
|
|
supplierId: "",
|
|
|
|
|
|
quotationDate: new Date(),
|
|
|
|
|
|
validUntil: new Date(),
|
|
|
|
|
|
status: QuotationStatusEnum.Draft,
|
|
|
|
|
|
totalAmount: 0,
|
|
|
|
|
|
currency: "TRY",
|
|
|
|
|
|
paymentTerms: "",
|
|
|
|
|
|
deliveryTerms: "",
|
|
|
|
|
|
items: [],
|
|
|
|
|
|
attachments: [],
|
|
|
|
|
|
notes: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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 addQuotationItem = () => {
|
|
|
|
|
|
const newItem: MmQuotationItem = {
|
|
|
|
|
|
id: `item-${Date.now()}`,
|
|
|
|
|
|
materialCode: "",
|
|
|
|
|
|
materialName: "",
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
quantity: 0,
|
|
|
|
|
|
unit: "",
|
|
|
|
|
|
unitPrice: 0,
|
|
|
|
|
|
totalPrice: 0,
|
|
|
|
|
|
specifications: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
items: [...(prev.items || []), newItem],
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeQuotationItem = (index: number) => {
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
items: prev.items?.filter((_, i) => i !== index) || [],
|
|
|
|
|
|
}));
|
|
|
|
|
|
calculateTotal();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updateQuotationItem = (
|
|
|
|
|
|
index: number,
|
|
|
|
|
|
field: keyof MmQuotationItem,
|
|
|
|
|
|
value: string | number | string[] | undefined
|
|
|
|
|
|
) => {
|
|
|
|
|
|
setFormData((prev) => {
|
|
|
|
|
|
const updatedItems =
|
|
|
|
|
|
prev.items?.map((item, i) => {
|
|
|
|
|
|
if (i === index) {
|
|
|
|
|
|
const updatedItem = { ...item, [field]: value };
|
|
|
|
|
|
// Auto-calculate total price when quantity or unit price changes
|
|
|
|
|
|
if (field === "quantity" || field === "unitPrice") {
|
|
|
|
|
|
updatedItem.totalPrice =
|
|
|
|
|
|
(updatedItem.quantity || 0) * (updatedItem.unitPrice || 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return updatedItem;
|
|
|
|
|
|
}
|
|
|
|
|
|
return item;
|
|
|
|
|
|
}) || [];
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
items: updatedItems,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Recalculate total amount
|
|
|
|
|
|
setTimeout(calculateTotal, 0);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const calculateTotal = () => {
|
|
|
|
|
|
const total =
|
|
|
|
|
|
formData.items?.reduce((sum, item) => sum + (item.totalPrice || 0), 0) ||
|
|
|
|
|
|
0;
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
totalAmount: total,
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSpecificationsChange = (index: number, value: string) => {
|
|
|
|
|
|
const specifications = value
|
|
|
|
|
|
.split("\n")
|
|
|
|
|
|
.filter((spec) => spec.trim() !== "");
|
|
|
|
|
|
updateQuotationItem(index, "specifications", specifications);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
// TODO: Implement save logic
|
|
|
|
|
|
console.log("Saving quotation:", formData);
|
|
|
|
|
|
navigate("/admin/supplychain/quotations");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
navigate("/admin/supplychain/quotations");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const isReadOnly = isView;
|
|
|
|
|
|
const pageTitle = isEdit
|
|
|
|
|
|
? "Teklifi Düzenle"
|
|
|
|
|
|
: isView
|
|
|
|
|
|
? "Teklif Detayları"
|
|
|
|
|
|
: "Yeni Teklif";
|
|
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
|
{/* Temel Bilgiler */}
|
|
|
|
|
|
<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">
|
|
|
|
|
|
<FaFileAlt className="mr-2 text-blue-600" />
|
|
|
|
|
|
Teklif 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">
|
|
|
|
|
|
Teklif Numarası
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
name="quotationNumber"
|
|
|
|
|
|
value={formData.quotationNumber || ""}
|
|
|
|
|
|
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="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Talep ID
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
name="requestId"
|
|
|
|
|
|
value={formData.requestId || ""}
|
|
|
|
|
|
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"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Talep Başlığı
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
name="requestTitle"
|
|
|
|
|
|
value={formData.requestTitle || ""}
|
|
|
|
|
|
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"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Talep Tipi
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
name="requestType"
|
|
|
|
|
|
value={formData.requestType || RequestTypeEnum.Material}
|
|
|
|
|
|
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={RequestTypeEnum.Material}>Malzeme</option>
|
|
|
|
|
|
<option value={RequestTypeEnum.Service}>Hizmet</option>
|
|
|
|
|
|
<option value={RequestTypeEnum.WorkCenter}>
|
|
|
|
|
|
İş Merkezi
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<option value={RequestTypeEnum.Maintenance}>Bakım</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
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="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Tedarikçi Adı
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
name="supplierName"
|
|
|
|
|
|
value={formData.supplier?.name || ""}
|
|
|
|
|
|
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"
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="flex items-center text-sm font-medium text-gray-700">
|
|
|
|
|
|
<FaCalendar className="mr-1" />
|
|
|
|
|
|
Teklif Tarihi
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
name="quotationDate"
|
|
|
|
|
|
value={
|
|
|
|
|
|
formData.quotationDate
|
|
|
|
|
|
? new Date(formData.quotationDate)
|
|
|
|
|
|
.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" />
|
|
|
|
|
|
Geçerlilik Tarihi
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
name="validUntil"
|
|
|
|
|
|
value={
|
|
|
|
|
|
formData.validUntil
|
|
|
|
|
|
? new Date(formData.validUntil)
|
|
|
|
|
|
.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">
|
|
|
|
|
|
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>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Ödeme Koşulları
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
name="paymentTerms"
|
|
|
|
|
|
value={formData.paymentTerms || ""}
|
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
|
readOnly={isReadOnly}
|
|
|
|
|
|
placeholder="ör: 30 gün vadeli"
|
|
|
|
|
|
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">
|
|
|
|
|
|
Teslimat Koşulları
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
name="deliveryTerms"
|
|
|
|
|
|
value={formData.deliveryTerms || ""}
|
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
|
readOnly={isReadOnly}
|
|
|
|
|
|
placeholder="ör: 15 gün içinde teslim"
|
|
|
|
|
|
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">
|
|
|
|
|
|
Teslimat Süresi (Gün)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
name="deliveryTime"
|
|
|
|
|
|
value={formData.deliveryTime || ""}
|
|
|
|
|
|
onChange={handleInputChange}
|
|
|
|
|
|
readOnly={isReadOnly}
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
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 className="mt-4">
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Notlar
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
name="notes"
|
|
|
|
|
|
value={formData.notes || ""}
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Teklif Kalemleri */}
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md p-4">
|
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
|
<h3 className="text-base font-medium text-gray-900">
|
|
|
|
|
|
Teklif Kalemleri
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
{!isReadOnly && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={addQuotationItem}
|
|
|
|
|
|
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={() => removeQuotationItem(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.materialCode}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateQuotationItem(
|
|
|
|
|
|
index,
|
|
|
|
|
|
"materialCode",
|
|
|
|
|
|
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.code}>
|
|
|
|
|
|
{material.name} ({material.code})
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-medium text-gray-600">
|
|
|
|
|
|
Malzeme Adı
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={item.materialName}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateQuotationItem(
|
|
|
|
|
|
index,
|
|
|
|
|
|
"materialName",
|
|
|
|
|
|
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">
|
|
|
|
|
|
Açıklama
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={item.description}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateQuotationItem(
|
|
|
|
|
|
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) =>
|
|
|
|
|
|
updateQuotationItem(
|
|
|
|
|
|
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
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={item.unit}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateQuotationItem(index, "unit", 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">
|
|
|
|
|
|
Birim Fiyat
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={item.unitPrice}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateQuotationItem(
|
|
|
|
|
|
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 Fiyat
|
|
|
|
|
|
</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">
|
|
|
|
|
|
Teslim Süresi (Gün)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={item.leadTime || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
updateQuotationItem(
|
|
|
|
|
|
index,
|
|
|
|
|
|
"leadTime",
|
|
|
|
|
|
parseInt(e.target.value) || undefined
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
readOnly={isReadOnly}
|
|
|
|
|
|
min="0"
|
|
|
|
|
|
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">
|
|
|
|
|
|
Spesifikasyonlar (her satıra bir spesifikasyon)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={item.specifications?.join("\n") || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
handleSpecificationsChange(index, e.target.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
readOnly={isReadOnly}
|
|
|
|
|
|
rows={1}
|
|
|
|
|
|
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 teklif 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-base font-medium text-gray-900 mb-3">
|
|
|
|
|
|
Durum Bilgileri
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
|
|
|
|
Durum
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
name="status"
|
|
|
|
|
|
value={formData.status || QuotationStatusEnum.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={QuotationStatusEnum.Draft}>Taslak</option>
|
|
|
|
|
|
<option value={QuotationStatusEnum.Pending}>
|
|
|
|
|
|
Beklemede
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<option value={QuotationStatusEnum.UnderReview}>
|
|
|
|
|
|
İncelemede
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<option value={QuotationStatusEnum.Submitted}>
|
|
|
|
|
|
Gönderildi
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<option value={QuotationStatusEnum.Approved}>
|
|
|
|
|
|
Onaylandı
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<option value={QuotationStatusEnum.Rejected}>
|
|
|
|
|
|
Reddedildi
|
|
|
|
|
|
</option>
|
|
|
|
|
|
<option value={QuotationStatusEnum.Expired}>
|
|
|
|
|
|
Süresi Doldu
|
|
|
|
|
|
</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="flex items-center text-sm font-medium text-gray-700">
|
|
|
|
|
|
<FaDollarSign className="mr-1" />
|
|
|
|
|
|
Toplam Tutar
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="mt-1 text-base font-semibold text-green-600">
|
|
|
|
|
|
{formData.totalAmount?.toLocaleString()}{" "}
|
|
|
|
|
|
{formData.currency}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Değerlendirme (sadece görüntüleme) */}
|
|
|
|
|
|
{isView && formData.evaluationScore && (
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md p-4">
|
|
|
|
|
|
<h3 className="text-base font-medium text-gray-900 mb-3">
|
|
|
|
|
|
Değerlendirme
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm font-medium text-gray-700">
|
|
|
|
|
|
Değerlendirme Puanı
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-base font-semibold text-blue-600">
|
|
|
|
|
|
{formData.evaluationScore}/100
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{formData.evaluatedBy && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm font-medium text-gray-700">
|
|
|
|
|
|
Değerlendiren
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-sm text-gray-600">
|
|
|
|
|
|
{formData.evaluatedBy}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{formData.evaluationComments && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm font-medium text-gray-700">
|
|
|
|
|
|
Değerlendirme Yorumları
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-sm text-gray-600 bg-gray-50 p-2 rounded">
|
|
|
|
|
|
{formData.evaluationComments}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Ekler */}
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md p-4">
|
|
|
|
|
|
<h3 className="text-base font-medium text-gray-900 mb-3 flex items-center">
|
|
|
|
|
|
<FaPaperclip className="mr-2 text-blue-600" />
|
|
|
|
|
|
Ekler
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
|
|
{formData.attachments && formData.attachments.length > 0 ? (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{formData.attachments.map((attachment: MmAttachment) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={attachment.id}
|
|
|
|
|
|
className="flex items-center justify-between p-1.5 bg-gray-50 rounded"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">
|
|
|
|
|
|
{attachment.fileName}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
|
{(attachment.fileSize / 1024).toFixed(1)} KB
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="text-center text-gray-500 text-sm py-3">
|
|
|
|
|
|
Henüz ek dosya yüklenmedi
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default QuotationForm;
|