erp-platform/ui/src/views/supplychain/components/QuotationForm.tsx
2025-09-17 11:58:20 +03:00

696 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
import { Container } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { getQuotationStatusText, getRequestTypeText } from '@/utils/erp'
import { mockCurrencies } from '@/mocks/mockCurrencies'
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(ROUTES_ENUM.protected.supplychain.quotations)
}
const handleCancel = () => {
navigate(ROUTES_ENUM.protected.supplychain.quotations)
}
const isReadOnly = isView
const pageTitle = isEdit ? 'Teklifi Düzenle' : isView ? 'Teklif Detayları' : 'Yeni Teklif'
return (
<Container>
<div className="space-y-2">
{/* 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>
<h2 className="text-2xl font-bold text-gray-900">{pageTitle}</h2>
</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"
>
{Object.values(RequestTypeEnum).map((type) => (
<option key={type} value={type}>
{getRequestTypeText(type)}
</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"
>
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</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">
ı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"
>
{Object.values(QuotationStatusEnum).map((status) => (
<option key={status} value={status}>
{getQuotationStatusText(status)}
</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>
</Container>
)
}
export default QuotationForm