erp-platform/ui/src/views/supplychain/components/OrderManagementForm.tsx
2025-09-17 12:46:58 +03:00

743 lines
31 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, { 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'
import { Container } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { getOrderStatusText, getPaymentTermsText } from '@/utils/erp'
import { mockCurrencies } from '@/mocks/mockCurrencies'
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(ROUTES_ENUM.protected.supplychain.orders)
}
const handleCancel = () => {
navigate(ROUTES_ENUM.protected.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 (
<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">
{/* 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"
>
{Object.values(PaymentTerms).map((term) => (
<option key={term} value={term}>
{getPaymentTermsText(term)}
</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"
>
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</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"
>
{Object.values(OrderStatusEnum).map((status) => (
<option key={status} value={status}>
{getOrderStatusText(status)}
</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>
</Container>
)
}
export default OrderManagementForm