Eksik permission, Formik
This commit is contained in:
parent
70b3469494
commit
c11781e2b9
12 changed files with 1439 additions and 1956 deletions
|
|
@ -1063,6 +1063,7 @@
|
|||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.BlogManagement.Category",
|
||||
|
|
@ -1111,6 +1112,301 @@
|
|||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.About",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Create",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Delete",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Export",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Import",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.About.Update",
|
||||
"ParentName": "App.About",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Services",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Create",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Delete",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Export",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Import",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Services.Update",
|
||||
"ParentName": "App.Services",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Orders.Products",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products.Create",
|
||||
"ParentName": "App.Orders.Products",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products.Delete",
|
||||
"ParentName": "App.Orders.Products",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products.Export",
|
||||
"ParentName": "App.Orders.Products",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products.Import",
|
||||
"ParentName": "App.Orders.Products",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.Products.Update",
|
||||
"ParentName": "App.Orders.Products",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PaymentMethods",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Orders.PaymentMethods",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PaymentMethods.Create",
|
||||
"ParentName": "App.Orders.PaymentMethods",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PaymentMethods.Delete",
|
||||
"ParentName": "App.Orders.PaymentMethods",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PaymentMethods.Export",
|
||||
"ParentName": "App.Orders.PaymentMethods",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PaymentMethods.Import",
|
||||
"ParentName": "App.Orders.PaymentMethods",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PaymentMethods.Update",
|
||||
"ParentName": "App.Orders.PaymentMethods",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.InstallmentOptions",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Orders.InstallmentOptions",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.InstallmentOptions.Create",
|
||||
"ParentName": "App.Orders.InstallmentOptions",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.InstallmentOptions.Delete",
|
||||
"ParentName": "App.Orders.InstallmentOptions",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.InstallmentOptions.Export",
|
||||
"ParentName": "App.Orders.InstallmentOptions",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.InstallmentOptions.Import",
|
||||
"ParentName": "App.Orders.InstallmentOptions",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.InstallmentOptions.Update",
|
||||
"ParentName": "App.Orders.InstallmentOptions",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PurchaseOrders",
|
||||
"ParentName": null,
|
||||
"DisplayName": "App.Orders.PurchaseOrders",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PurchaseOrders.Create",
|
||||
"ParentName": "App.Orders.PurchaseOrders",
|
||||
"DisplayName": "Create",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PurchaseOrders.Delete",
|
||||
"ParentName": "App.Orders.PurchaseOrders",
|
||||
"DisplayName": "Delete",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PurchaseOrders.Export",
|
||||
"ParentName": "App.Orders.PurchaseOrders",
|
||||
"DisplayName": "Export",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PurchaseOrders.Import",
|
||||
"ParentName": "App.Orders.PurchaseOrders",
|
||||
"DisplayName": "Import",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.Orders.PurchaseOrders.Update",
|
||||
"ParentName": "App.Orders.PurchaseOrders",
|
||||
"DisplayName": "Update",
|
||||
"IsEnabled": true,
|
||||
"MultiTenancySide": 2
|
||||
},
|
||||
|
||||
{
|
||||
"GroupName": "App.Saas",
|
||||
"Name": "App.BlogManagement.Posts",
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ define(['./workbox-a959eb95'], (function (workbox) { 'use strict';
|
|||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "/index.html",
|
||||
"revision": "0.v5arcquq4lo"
|
||||
"revision": "0.sjlnuata16g"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@ export const abpConfigModel: AbpConfigModel = {
|
|||
|
||||
actions.setConfig(result.data)
|
||||
|
||||
//Eğer login değilse
|
||||
if (result.data.currentUser.isAuthenticated) {
|
||||
await actions.getMenu()
|
||||
}
|
||||
}),
|
||||
texts: undefined,
|
||||
setTexts: action((state, payload) => {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export interface WmLocation {
|
|||
// Lokasyon
|
||||
id: string
|
||||
warehouseId: string
|
||||
zoneId?: string
|
||||
zoneId: string
|
||||
locationCode: string
|
||||
name: string
|
||||
description?: string
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { MenuItem } from '@/@types/menu'
|
||||
import isDisabled from '@/components/ui/DatePicker/tables/components/props/isDisabled'
|
||||
import { getMenus, MenuService } from '@/services/menu.service'
|
||||
import { useStoreActions } from '@/store/store'
|
||||
import { useState, useEffect } from 'react'
|
||||
import useAuth from './useAuth'
|
||||
|
||||
export const useMenuData = () => {
|
||||
const [menuItems, setMenuItems] = useState<MenuItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const { getConfig } = useStoreActions((a) => a.abpConfig)
|
||||
const { authenticated } = useAuth()
|
||||
|
||||
const buildHierarchy = (items: MenuItem[]): MenuItem[] => {
|
||||
const itemMap = new Map<string, MenuItem>()
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function RolesPermission({
|
|||
|
||||
setTimeout(async () => {
|
||||
getConfig(true)
|
||||
}, 2000)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
function getPermissionsWithGroupName(groups: PermissionGroupDto[]): PermissionWithGroupName[] {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import React from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import { Formik, Form, Field } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { FaSave, FaTimes } from 'react-icons/fa'
|
||||
import { SerialStatusEnum, MmSerialNumber, MmMaterial } from '../../../types/mm'
|
||||
import { getSerialStatusText } from '@/utils/erp'
|
||||
|
||||
import { Input, Checkbox, Button } from '@/components/ui'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
export interface SerialFormProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
|
|
@ -31,21 +34,33 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
|||
initial = null,
|
||||
mode = 'create',
|
||||
}) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
materialId: '',
|
||||
serialNumber: '',
|
||||
lotId: lots.length ? lots[0].id : '',
|
||||
productionDate: '',
|
||||
warrantyExpiryDate: '',
|
||||
currentLocationId: '',
|
||||
status: SerialStatusEnum.Available,
|
||||
isActive: true,
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: async (values) => {
|
||||
if (!isOpen) return null
|
||||
|
||||
const initialValues = {
|
||||
materialId: initial?.materialId || '',
|
||||
serialNumber: initial?.serialNumber || '',
|
||||
lotId: initial?.lotId || '',
|
||||
productionDate: initial?.productionDate
|
||||
? new Date(initial.productionDate).toISOString().slice(0, 10)
|
||||
: '',
|
||||
warrantyExpiryDate: initial?.warrantyExpiryDate
|
||||
? new Date(initial.warrantyExpiryDate).toISOString().slice(0, 10)
|
||||
: '',
|
||||
currentLocationId: initial?.currentLocationId || '',
|
||||
status: initial?.status || SerialStatusEnum.Available,
|
||||
isActive: initial?.isActive ?? true,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black opacity-40" onClick={onClose} />
|
||||
<div className="relative bg-white rounded-lg shadow-lg w-full max-w-2xl mx-4">
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={async (values, helpers) => {
|
||||
const newSerial: MmSerialNumber = {
|
||||
id: (initial && initial.id) || Date.now().toString(),
|
||||
id: initial?.id || Date.now().toString(),
|
||||
materialId: values.materialId,
|
||||
serialNumber: values.serialNumber,
|
||||
lotId: values.lotId || undefined,
|
||||
|
|
@ -58,7 +73,6 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
|||
isActive: !!values.isActive,
|
||||
}
|
||||
|
||||
// simulate API
|
||||
await new Promise((r) => setTimeout(r, 300))
|
||||
if (mode === 'edit' && onUpdate) {
|
||||
onUpdate(newSerial)
|
||||
|
|
@ -66,153 +80,156 @@ const SerialForm: React.FC<SerialFormProps> = ({
|
|||
onSave(newSerial)
|
||||
}
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
// sync initial values when editing/viewing
|
||||
React.useEffect(() => {
|
||||
if (initial) {
|
||||
const src = initial
|
||||
formik.setValues({
|
||||
materialId: src.materialId || '',
|
||||
serialNumber: src.serialNumber || '',
|
||||
lotId: src.lotId || (lots.length ? lots[0].id : ''),
|
||||
productionDate: src.productionDate
|
||||
? new Date(src.productionDate).toISOString().slice(0, 10)
|
||||
: '',
|
||||
warrantyExpiryDate: src.warrantyExpiryDate
|
||||
? new Date(src.warrantyExpiryDate).toISOString().slice(0, 10)
|
||||
: '',
|
||||
currentLocationId: src.currentLocationId || '',
|
||||
status: src.status || SerialStatusEnum.Available,
|
||||
isActive: !!src.isActive,
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initial])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black opacity-40" onClick={onClose} />
|
||||
<div className="relative bg-white rounded-lg shadow-lg w-full max-w-2xl mx-4">
|
||||
<form onSubmit={formik.handleSubmit} className="space-y-2 p-3">
|
||||
helpers.setSubmitting(false)
|
||||
}}
|
||||
>
|
||||
{({ values, errors, touched, setFieldValue, isSubmitting }) => (
|
||||
<Form className="space-y-2 p-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-2 border-b">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
<h2 className="text-lg font-bold text-gray-900">
|
||||
{mode === 'create'
|
||||
? 'Yeni Seri Kaydı'
|
||||
: mode === 'edit'
|
||||
? 'Seri Düzenle'
|
||||
: 'Seri Detayı'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
{mode === 'create'
|
||||
? 'Seri numarası girin'
|
||||
: mode === 'edit'
|
||||
? 'Mevcut seri bilgisini güncelleyin'
|
||||
: 'Seri bilgileri (sadece gösterim)'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-2.5 py-1 text-sm border rounded-md bg-white hover:bg-gray-50"
|
||||
variant="default"
|
||||
className="flex items-center"
|
||||
>
|
||||
<FaTimes className="inline mr-1 h-3 w-3" /> Kapat
|
||||
</button>
|
||||
<FaTimes className="mr-1" />
|
||||
<span>Kapat</span>
|
||||
</Button>
|
||||
{mode !== 'view' && (
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
className="px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
loading={isSubmitting}
|
||||
icon={<FaSave className="w-4 h-4" />}
|
||||
>
|
||||
<FaSave className="inline mr-1 h-3 w-3" /> Kaydet
|
||||
</button>
|
||||
Kaydet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 p-2 gap-x-3 gap-y-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Malzeme *</label>
|
||||
<select
|
||||
{...formik.getFieldProps('materialId')}
|
||||
disabled={mode === 'view'}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="">Seçiniz...</option>
|
||||
{materials.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.code} - {m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Grid Form */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{/* Material */}
|
||||
<div className="grid gap-1">
|
||||
<label className="block text-sm text-gray-700">Malzeme *</label>
|
||||
<Select
|
||||
isDisabled={mode === 'view'}
|
||||
options={materials.map((m) => ({
|
||||
value: m.id,
|
||||
label: `${m.code} - ${m.name}`,
|
||||
}))}
|
||||
value={materials
|
||||
.map((m) => ({ value: m.id, label: `${m.code} - ${m.name}` }))
|
||||
.find((opt) => opt.value === values.materialId)}
|
||||
onChange={(opt) => setFieldValue('materialId', opt?.value || '')}
|
||||
/>
|
||||
{touched.materialId && errors.materialId && (
|
||||
<p className="text-xs text-red-500 mt-1">{errors.materialId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Seri Numarası *</label>
|
||||
<input
|
||||
type="text"
|
||||
{...formik.getFieldProps('serialNumber')}
|
||||
{/* Serial */}
|
||||
<div className="grid gap-1">
|
||||
<label className="block text-sm text-gray-700">Seri No *</label>
|
||||
<Field
|
||||
name="serialNumber"
|
||||
placeholder="Seri numarası"
|
||||
disabled={mode === 'view'}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||
component={Input}
|
||||
size="sm"
|
||||
/>
|
||||
{touched.serialNumber && errors.serialNumber && (
|
||||
<p className="text-xs text-red-500 mt-1">{errors.serialNumber}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Lot */}
|
||||
<div className="grid gap-1">
|
||||
<label className="block text-sm text-gray-700">Lot</label>
|
||||
<Select
|
||||
size="sm"
|
||||
isDisabled={mode === 'view'}
|
||||
isClearable
|
||||
options={lots.map((l) => ({ value: l.id, label: l.label }))}
|
||||
value={lots
|
||||
.map((l) => ({ value: l.id, label: l.label }))
|
||||
.find((opt) => opt.value === values.lotId)}
|
||||
onChange={(opt) => setFieldValue('lotId', opt?.value || '')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Lot</label>
|
||||
<select
|
||||
{...formik.getFieldProps('lotId')}
|
||||
disabled={mode === 'view'}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="">Seçiniz...</option>
|
||||
{lots.map((l) => (
|
||||
<option key={l.id} value={l.id}>
|
||||
{l.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Status */}
|
||||
<div className="grid gap-1">
|
||||
<label className="block text-sm text-gray-700">Durum</label>
|
||||
<Select
|
||||
size="sm"
|
||||
isDisabled={mode === 'view'}
|
||||
options={Object.values(SerialStatusEnum).map((s) => ({
|
||||
value: s,
|
||||
label: getSerialStatusText(s),
|
||||
}))}
|
||||
value={Object.values(SerialStatusEnum)
|
||||
.map((s) => ({ value: s, label: getSerialStatusText(s) }))
|
||||
.find((opt) => opt.value === values.status)}
|
||||
onChange={(opt) => setFieldValue('status', opt?.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Üretim Tarihi</label>
|
||||
<input
|
||||
{/* Production Date */}
|
||||
<div className="grid gap-1">
|
||||
<label className="block text-sm text-gray-700">Üretim Tarihi</label>
|
||||
<Field
|
||||
name="productionDate"
|
||||
type="date"
|
||||
{...formik.getFieldProps('productionDate')}
|
||||
disabled={mode === 'view'}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||
component={Input}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Garanti Bitiş</label>
|
||||
<input
|
||||
{/* Warranty Date */}
|
||||
<div className="grid gap-1">
|
||||
<label className="block text-sm text-gray-700">Garanti Bitiş</label>
|
||||
<Field
|
||||
name="warrantyExpiryDate"
|
||||
type="date"
|
||||
{...formik.getFieldProps('warrantyExpiryDate')}
|
||||
disabled={mode === 'view'}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||
component={Input}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Durum</label>
|
||||
<select
|
||||
{...formik.getFieldProps('status')}
|
||||
{/* Active Checkbox */}
|
||||
<div className="pt-2">
|
||||
<Field
|
||||
name="isActive"
|
||||
type="checkbox"
|
||||
component={Checkbox}
|
||||
disabled={mode === 'view'}
|
||||
className="w-full border rounded-md px-2 py-1 text-sm"
|
||||
size="sm"
|
||||
>
|
||||
{Object.values(SerialStatusEnum).map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getSerialStatusText(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
Aktif
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -733,7 +733,7 @@ const WarehouseDefinitions: React.FC = () => {
|
|||
<FaMapMarkerAlt className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900">{location.name}</h3>
|
||||
<div className="text-xs font-medium text-gray-900">{location.name}</div>
|
||||
<p className="text-sm text-gray-500">{location.locationCode}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,515 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { FaSave, FaTimes, FaBox, FaMapMarkerAlt, FaLayerGroup, FaBuilding } from 'react-icons/fa'
|
||||
import LoadingSpinner from '../../../components/common/LoadingSpinner'
|
||||
import { WmWarehouse, WarehouseTypeEnum } from '../../../types/wm'
|
||||
import { SecurityLevelEnum } from '../../../types/mrp'
|
||||
import { mockWarehouses } from '../../../mocks/mockWarehouses'
|
||||
import { Container } from '@/components/shared'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { getWarehouseTypeText } from '@/utils/erp'
|
||||
|
||||
interface ValidationErrors {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const WarehouseForm: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const isEdit = Boolean(id)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||
|
||||
const [formData, setFormData] = useState<WmWarehouse>({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
address: {
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postalCode: '',
|
||||
country: 'Türkiye',
|
||||
},
|
||||
warehouseType: WarehouseTypeEnum.RawMaterials,
|
||||
locations: [],
|
||||
isMainWarehouse: false,
|
||||
capacity: 0,
|
||||
currentUtilization: 0,
|
||||
zones: [],
|
||||
isActive: true,
|
||||
securityLevel: SecurityLevelEnum.Low,
|
||||
temperatureControlled: false,
|
||||
managerId: 0,
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
})
|
||||
|
||||
const loadFormData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (isEdit && id) {
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const warehouse = mockWarehouses.find((w) => w.id === id)
|
||||
if (warehouse) {
|
||||
setFormData(warehouse)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading form data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [isEdit, id])
|
||||
|
||||
useEffect(() => {
|
||||
loadFormData()
|
||||
}, [loadFormData])
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: ValidationErrors = {}
|
||||
|
||||
if (!formData.code.trim()) {
|
||||
newErrors.code = 'Depo kodu zorunludur'
|
||||
}
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Depo adı zorunludur'
|
||||
}
|
||||
if (!formData.warehouseType) {
|
||||
newErrors.warehouseType = 'Depo tipi seçilmelidir'
|
||||
}
|
||||
if (!formData.address?.street.trim()) {
|
||||
newErrors['address.street'] = 'Adres zorunludur'
|
||||
}
|
||||
if (!formData.address?.city.trim()) {
|
||||
newErrors['address.city'] = 'Şehir zorunludur'
|
||||
}
|
||||
if (!formData.address?.country.trim()) {
|
||||
newErrors['address.country'] = 'Ülke zorunludur'
|
||||
}
|
||||
if (formData.capacity <= 0) {
|
||||
newErrors.capacity = "Kapasite 0'dan büyük olmalıdır"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: any) => {
|
||||
setFormData((prev) => {
|
||||
const updated = { ...prev }
|
||||
const keys = field.split('.')
|
||||
let current: any = updated
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
current[key] = value
|
||||
} else {
|
||||
current[key] = { ...current[key] }
|
||||
current = current[key]
|
||||
}
|
||||
})
|
||||
return updated
|
||||
})
|
||||
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: '' }))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
console.log('Warehouse data:', {
|
||||
...formData,
|
||||
id: isEdit ? id : undefined,
|
||||
})
|
||||
|
||||
// Show success message
|
||||
alert(isEdit ? 'Depo başarıyla güncellendi!' : 'Depo başarıyla oluşturuldu!')
|
||||
|
||||
// Navigate back to list
|
||||
navigate(ROUTES_ENUM.protected.warehouse.warehouses)
|
||||
} catch (error) {
|
||||
console.error('Error saving warehouse:', error)
|
||||
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate(ROUTES_ENUM.protected.warehouse.warehouses)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">
|
||||
{isEdit ? 'Depo Düzenle' : 'Yeni Depo'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{isEdit ? 'Mevcut depo bilgilerini güncelleyin' : 'Yeni depo bilgilerini girin'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Basic Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
||||
<FaBuilding className="w-4 h-4 mr-2 text-gray-600" />
|
||||
Genel Bilgiler
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo Kodu *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.code}
|
||||
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.code
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Örn: WH001"
|
||||
/>
|
||||
{errors.code && <p className="mt-1 text-sm text-red-600">{errors.code}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo Tipi *
|
||||
</label>
|
||||
<select
|
||||
value={formData.warehouseType}
|
||||
onChange={(e) => handleInputChange('warehouseType', e.target.value)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.warehouseType
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
<option value="">Tip seçin</option>
|
||||
{Object.values(WarehouseTypeEnum).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getWarehouseTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.warehouseType && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.warehouseType}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Depo Adı *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.name
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Depo adı"
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red-600">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Açıklama</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
rows={3}
|
||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="Depo açıklaması"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kapasite (m³) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={formData.capacity}
|
||||
onChange={(e) => handleInputChange('capacity', parseFloat(e.target.value) || 0)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.capacity
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="0"
|
||||
/>
|
||||
{errors.capacity && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.capacity}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6 pt-6">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isMainWarehouse"
|
||||
checked={formData.isMainWarehouse}
|
||||
onChange={(e) => handleInputChange('isMainWarehouse', e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isMainWarehouse" className="ml-2 block text-sm text-gray-900">
|
||||
Ana Depo
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
||||
<FaMapMarkerAlt className="w-4 h-4 mr-2 text-gray-600" />
|
||||
Adres Bilgileri
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Adres *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.street}
|
||||
onChange={(e) => handleInputChange('address.street', e.target.value)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors['address.street']
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Sokak, cadde, mahalle"
|
||||
/>
|
||||
{errors['address.street'] && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors['address.street']}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Şehir *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.city}
|
||||
onChange={(e) => handleInputChange('address.city', e.target.value)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors['address.city']
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Şehir"
|
||||
/>
|
||||
{errors['address.city'] && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors['address.city']}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">İl/Bölge</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.state}
|
||||
onChange={(e) => handleInputChange('address.state', e.target.value)}
|
||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="İl/Bölge"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Posta Kodu</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.postalCode}
|
||||
onChange={(e) => handleInputChange('address.postalCode', e.target.value)}
|
||||
className="block w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="34000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Ülke *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.country}
|
||||
onChange={(e) => handleInputChange('address.country', e.target.value)}
|
||||
className={`block w-full px-3 py-1.5 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors['address.country']
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Ülke"
|
||||
/>
|
||||
{errors['address.country'] && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors['address.country']}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capacity Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
||||
<FaBox className="w-4 h-4 mr-2 text-gray-600" />
|
||||
Kapasite Bilgileri
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Toplam Kapasite:</span>
|
||||
<div className="text-lg font-semibold text-blue-600">
|
||||
{formData.capacity.toLocaleString()} m³
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Mevcut Kullanım:</span>
|
||||
<div className="text-lg font-semibold text-green-600">0 m³ (0%)</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">Müsait Kapasite:</span>
|
||||
<div className="text-lg font-semibold text-gray-600">
|
||||
{formData.capacity.toLocaleString()} m³
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div className="bg-blue-600 h-2 rounded-full" style={{ width: '0%' }} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">Kullanım oranı: 0%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warehouse Type Information */}
|
||||
{formData.warehouseType && (
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 flex items-center">
|
||||
<FaLayerGroup className="w-5 h-5 mr-2" />
|
||||
Depo Tipi Özellikleri
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4">
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-4">
|
||||
<div className="flex">
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-blue-700">
|
||||
{formData.warehouseType === WarehouseTypeEnum.RawMaterials &&
|
||||
'Hammadde depolama için özel koşullara sahip olacaktır.'}
|
||||
{formData.warehouseType === WarehouseTypeEnum.FinishedGoods &&
|
||||
'Mamul depolama ve sevkiyat operasyonları için optimize edilecektir.'}
|
||||
{formData.warehouseType === WarehouseTypeEnum.SpareParts &&
|
||||
'Yedek parça yönetimi için özel raflar ve kategorilendirme yapılacaktır.'}
|
||||
{formData.warehouseType === WarehouseTypeEnum.Returns &&
|
||||
'Konsinye stok yönetimi için ayrı izleme sistemi kullanılacaktır.'}
|
||||
{formData.warehouseType === WarehouseTypeEnum.Quarantine &&
|
||||
'Karantina prosedürleri ve onay mekanizmaları aktif olacaktır.'}
|
||||
{formData.warehouseType === WarehouseTypeEnum.Transit &&
|
||||
'Sıcaklık kontrollü depolama sistemi kurulacaktır.'}
|
||||
{formData.warehouseType === WarehouseTypeEnum.WorkInProgress &&
|
||||
'Tehlikeli madde depolama güvenlik protokolleri uygulanacaktır.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex items-center justify-end space-x-2 bg-gray-50 px-4 py-3 rounded-b-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="inline-flex items-center px-4 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<FaTimes className="w-4 h-4 mr-2" />
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="inline-flex items-center px-4 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Kaydediliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaSave className="w-4 h-4 mr-2" />
|
||||
{isEdit ? 'Güncelle' : 'Kaydet'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default WarehouseForm
|
||||
|
|
@ -1,23 +1,48 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
WmLocation,
|
||||
LocationTypeEnum,
|
||||
WmWarehouse,
|
||||
WmZone,
|
||||
} from "../../../../types/wm";
|
||||
import { FaTimes, FaSave, FaMapMarkerAlt } from "react-icons/fa";
|
||||
import { getLocationTypeText } from "../../../../utils/erp";
|
||||
import React, { useState } from 'react'
|
||||
import { Formik, Form, Field, FieldProps, getIn } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { WmLocation, LocationTypeEnum, WmWarehouse, WmZone } from '../../../../types/wm'
|
||||
import { FaTimes, FaSave, FaMapMarkerAlt } from 'react-icons/fa'
|
||||
import { getLocationTypeText } from '../../../../utils/erp'
|
||||
|
||||
import { Input, Checkbox, FormContainer, FormItem } from '@/components/ui'
|
||||
import Select from '@/components/ui/Select'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface LocationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (location: Partial<WmLocation>) => void;
|
||||
location?: WmLocation | null;
|
||||
mode: "create" | "edit" | "view";
|
||||
warehouses: WmWarehouse[];
|
||||
zones: WmZone[];
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (location: Partial<WmLocation>) => void
|
||||
location?: WmLocation | null
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
warehouses: WmWarehouse[]
|
||||
zones: WmZone[]
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
warehouseId: Yup.string().required('Depo seçimi zorunludur'),
|
||||
zoneId: Yup.string().required('Bölge seçimi zorunludur'),
|
||||
locationCode: Yup.string().required('Lokasyon kodu zorunludur'),
|
||||
name: Yup.string().required('Lokasyon adı zorunludur'),
|
||||
capacity: Yup.number()
|
||||
.moreThan(0, "Kapasite 0'dan büyük olmalıdır")
|
||||
.required('Kapasite zorunludur'),
|
||||
currentStock: Yup.number().test(
|
||||
'stock-vs-capacity',
|
||||
'Mevcut stok kapasiteyi aşamaz',
|
||||
function (value) {
|
||||
return (value || 0) <= (this.parent.capacity || 0)
|
||||
},
|
||||
),
|
||||
dimensions: Yup.object({
|
||||
length: Yup.number().min(0).nullable(),
|
||||
width: Yup.number().min(0).nullable(),
|
||||
height: Yup.number().min(0).nullable(),
|
||||
weight: Yup.number().min(0).nullable(),
|
||||
unit: Yup.string().required(),
|
||||
}),
|
||||
})
|
||||
|
||||
const LocationModal: React.FC<LocationModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
|
@ -27,167 +52,42 @@ const LocationModal: React.FC<LocationModalProps> = ({
|
|||
warehouses,
|
||||
zones,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Partial<WmLocation>>({
|
||||
warehouseId: "",
|
||||
zoneId: "",
|
||||
locationCode: "",
|
||||
name: "",
|
||||
description: "",
|
||||
locationType: LocationTypeEnum.Shelf,
|
||||
capacity: 0,
|
||||
currentStock: 0,
|
||||
const [restrictionInput, setRestrictionInput] = useState('')
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const initialValues: Partial<WmLocation> = {
|
||||
warehouseId: location?.warehouseId || '',
|
||||
zoneId: location?.zoneId || '', // ✅ boş string, null yerine
|
||||
locationCode: location?.locationCode || '',
|
||||
name: location?.name || '',
|
||||
description: location?.description || '',
|
||||
locationType: location?.locationType || LocationTypeEnum.Shelf,
|
||||
capacity: location?.capacity || 0,
|
||||
currentStock: location?.currentStock || 0,
|
||||
dimensions: {
|
||||
length: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
weight: 0,
|
||||
unit: "kg",
|
||||
length: location?.dimensions?.length || 0,
|
||||
width: location?.dimensions?.width || 0,
|
||||
height: location?.dimensions?.height || 0,
|
||||
weight: location?.dimensions?.weight || 0,
|
||||
unit: location?.dimensions?.unit || 'kg',
|
||||
},
|
||||
restrictions: [],
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [restrictionInput, setRestrictionInput] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (location) {
|
||||
setFormData(location);
|
||||
} else {
|
||||
setFormData({
|
||||
warehouseId: "",
|
||||
zoneId: "",
|
||||
locationCode: "",
|
||||
name: "",
|
||||
description: "",
|
||||
locationType: LocationTypeEnum.Shelf,
|
||||
capacity: 0,
|
||||
currentStock: 0,
|
||||
dimensions: {
|
||||
length: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
weight: 0,
|
||||
unit: "kg",
|
||||
},
|
||||
restrictions: [],
|
||||
isActive: true,
|
||||
});
|
||||
restrictions: location?.restrictions || [],
|
||||
isActive: location?.isActive ?? true,
|
||||
}
|
||||
setErrors({});
|
||||
setRestrictionInput("");
|
||||
}, [location, isOpen]);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.warehouseId?.trim()) {
|
||||
newErrors.warehouseId = "Depo seçimi zorunludur";
|
||||
}
|
||||
if (!formData.locationCode?.trim()) {
|
||||
newErrors.locationCode = "Lokasyon kodu zorunludur";
|
||||
}
|
||||
if (!formData.name?.trim()) {
|
||||
newErrors.name = "Lokasyon adı zorunludur";
|
||||
}
|
||||
if (!formData.capacity || formData.capacity <= 0) {
|
||||
newErrors.capacity = "Kapasite 0'dan büyük olmalıdır";
|
||||
}
|
||||
if (
|
||||
formData.currentStock &&
|
||||
formData.currentStock > (formData.capacity || 0)
|
||||
) {
|
||||
newErrors.currentStock = "Mevcut stok kapasiteyi aşamaz";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (mode === "view") return;
|
||||
|
||||
if (validateForm()) {
|
||||
onSave(formData);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | boolean | string[]
|
||||
) => {
|
||||
if (field.includes(".")) {
|
||||
const [parent, child] = field.split(".");
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[parent]: {
|
||||
...(prev[parent as keyof typeof prev] as object),
|
||||
[child]: value,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
}
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const getModalTitle = () => {
|
||||
switch (mode) {
|
||||
case "create":
|
||||
return "Yeni Lokasyon Oluştur";
|
||||
case "edit":
|
||||
return "Lokasyon Düzenle";
|
||||
case "view":
|
||||
return "Lokasyon Görüntüle";
|
||||
case 'create':
|
||||
return 'Yeni Lokasyon Oluştur'
|
||||
case 'edit':
|
||||
return 'Lokasyon Düzenle'
|
||||
case 'view':
|
||||
return 'Lokasyon Görüntüle'
|
||||
default:
|
||||
return "Lokasyon";
|
||||
return 'Lokasyon'
|
||||
}
|
||||
};
|
||||
|
||||
const addRestriction = () => {
|
||||
if (
|
||||
restrictionInput.trim() &&
|
||||
!formData.restrictions?.includes(restrictionInput.trim())
|
||||
) {
|
||||
const newRestrictions = [
|
||||
...(formData.restrictions || []),
|
||||
restrictionInput.trim(),
|
||||
];
|
||||
handleInputChange("restrictions", newRestrictions);
|
||||
setRestrictionInput("");
|
||||
}
|
||||
};
|
||||
|
||||
const removeRestriction = (index: number) => {
|
||||
const newRestrictions =
|
||||
formData.restrictions?.filter((_, i) => i !== index) || [];
|
||||
handleInputChange("restrictions", newRestrictions);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addRestriction();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const selectedWarehouse = warehouses.find(
|
||||
(w) => w.id === formData.warehouseId
|
||||
);
|
||||
const selectedZone = zones.find((z) => z.id === formData.zoneId);
|
||||
const availableZones = zones.filter(
|
||||
(zone) => zone.warehouseId === formData.warehouseId
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
|
|
@ -198,319 +98,286 @@ const LocationModal: React.FC<LocationModalProps> = ({
|
|||
<div className="p-2 bg-orange-100 rounded-lg">
|
||||
<FaMapMarkerAlt className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{getModalTitle()}
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">{getModalTitle()}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
||||
{/* Warehouse and Zone Selection */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo *
|
||||
</label>
|
||||
<select
|
||||
value={formData.warehouseId || ""}
|
||||
onChange={(e) => {
|
||||
handleInputChange("warehouseId", e.target.value);
|
||||
handleInputChange("zoneId", ""); // Reset zone when warehouse changes
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, helpers) => {
|
||||
if (mode !== 'view') {
|
||||
console.log('Submitting values:', values)
|
||||
onSave(values)
|
||||
onClose()
|
||||
}
|
||||
helpers.setSubmitting(false)
|
||||
}}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="">Depo seçin</option>
|
||||
{warehouses.map((warehouse) => (
|
||||
<option key={warehouse.id} value={warehouse.id}>
|
||||
{warehouse.name} ({warehouse.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.warehouseId && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.warehouseId}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{({ errors, touched, isSubmitting, values, setFieldValue }) => {
|
||||
const availableZones = zones.filter((z) => z.warehouseId === values.warehouseId)
|
||||
const selectedWarehouse = warehouses.find((w) => w.id === values.warehouseId)
|
||||
const selectedZone = zones.find((z) => z.id === values.zoneId)
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bölge
|
||||
</label>
|
||||
<select
|
||||
value={formData.zoneId || ""}
|
||||
onChange={(e) => handleInputChange("zoneId", e.target.value)}
|
||||
disabled={mode === "view" || !formData.warehouseId}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
<option value="">Bölge seçin (opsiyonel)</option>
|
||||
{availableZones.map((zone) => (
|
||||
<option key={zone.id} value={zone.id}>
|
||||
{zone.name} ({zone.zoneCode})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
const addRestriction = () => {
|
||||
if (
|
||||
restrictionInput.trim() &&
|
||||
!values.restrictions?.includes(restrictionInput.trim())
|
||||
) {
|
||||
setFieldValue('restrictions', [
|
||||
...(values.restrictions || []),
|
||||
restrictionInput.trim(),
|
||||
])
|
||||
setRestrictionInput('')
|
||||
}
|
||||
}
|
||||
|
||||
{/* Basic Information */}
|
||||
const removeRestriction = (index: number) => {
|
||||
setFieldValue(
|
||||
'restrictions',
|
||||
values.restrictions?.filter((_, i) => i !== index) || [],
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form className="p-4 space-y-4">
|
||||
<FormContainer size="sm">
|
||||
{/* Warehouse and Zone */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Lokasyon Kodu *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.locationCode || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("locationCode", e.target.value)
|
||||
}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="A01-01-01"
|
||||
/>
|
||||
{errors.locationCode && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.locationCode}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Lokasyon Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name || ""}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="A Blok 1. Koridor 1. Raf"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Açıklama
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
disabled={mode === "view"}
|
||||
rows={3}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="Lokasyon açıklaması..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location Type and Capacity */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Lokasyon Tipi
|
||||
</label>
|
||||
<select
|
||||
value={formData.locationType || LocationTypeEnum.Shelf}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"locationType",
|
||||
e.target.value as LocationTypeEnum
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<FormItem
|
||||
label="Depo *"
|
||||
invalid={!!(errors.warehouseId && touched.warehouseId)}
|
||||
errorMessage={errors.warehouseId}
|
||||
>
|
||||
{Object.values(LocationTypeEnum).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getLocationTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Field name="warehouseId">
|
||||
{({ field, form }: FieldProps<string>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable
|
||||
isDisabled={mode === 'view'}
|
||||
options={warehouses.map((w) => ({
|
||||
value: w.id,
|
||||
label: `${w.name} (${w.code})`,
|
||||
}))}
|
||||
value={warehouses
|
||||
.map((w) => ({
|
||||
value: w.id,
|
||||
label: `${w.name} (${w.code})`,
|
||||
}))
|
||||
.find((opt) => opt.value === values.warehouseId)}
|
||||
onChange={(opt) => {
|
||||
form.setFieldValue('warehouseId', opt?.value || '')
|
||||
form.setFieldValue('zoneId', '') // ✅ reset
|
||||
form.setFieldTouched('zoneId', false) // ✅ touched reset
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="Bölge">
|
||||
<Field name="zoneId">
|
||||
{({ field, form }: FieldProps<string | null>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable
|
||||
isDisabled={mode === 'view' || !values.warehouseId}
|
||||
options={availableZones.map((z) => ({
|
||||
value: z.id,
|
||||
label: `${z.name} (${z.zoneCode})`,
|
||||
}))}
|
||||
value={
|
||||
values.zoneId
|
||||
? availableZones
|
||||
.map((z) => ({
|
||||
value: z.id,
|
||||
label: `${z.name} (${z.zoneCode})`,
|
||||
}))
|
||||
.find((opt) => opt.value === values.zoneId) || null
|
||||
: null // ✅ boş string / null durumunda null döndür
|
||||
}
|
||||
onChange={(opt) => form.setFieldValue('zoneId', opt?.value ?? '')}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kapasite *
|
||||
</label>
|
||||
<input
|
||||
{/* Location Code + Name */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem
|
||||
label="Lokasyon Kodu *"
|
||||
invalid={!!(errors.locationCode && touched.locationCode)}
|
||||
errorMessage={errors.locationCode}
|
||||
>
|
||||
<Field
|
||||
name="locationCode"
|
||||
placeholder="A01-01-01"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Lokasyon Adı *"
|
||||
invalid={!!(errors.name && touched.name)}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="A Blok 1. Koridor 1. Raf"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<FormItem label="Açıklama">
|
||||
<Field
|
||||
name="description"
|
||||
as="textarea"
|
||||
rows={3}
|
||||
placeholder="Lokasyon açıklaması..."
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
{/* Location Type + Capacity + Current Stock */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<FormItem label="Lokasyon Tipi">
|
||||
<Field name="locationType">
|
||||
{({ field, form }: FieldProps<LocationTypeEnum>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={false}
|
||||
isDisabled={mode === 'view'}
|
||||
options={Object.values(LocationTypeEnum).map((t) => ({
|
||||
value: t,
|
||||
label: getLocationTypeText(t),
|
||||
}))}
|
||||
value={Object.values(LocationTypeEnum)
|
||||
.map((t) => ({
|
||||
value: t,
|
||||
label: getLocationTypeText(t),
|
||||
}))
|
||||
.find((opt) => opt.value === values.locationType)}
|
||||
onChange={(opt) => form.setFieldValue(field.name, opt?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Kapasite *"
|
||||
invalid={!!(errors.capacity && touched.capacity)}
|
||||
errorMessage={errors.capacity}
|
||||
>
|
||||
<Field
|
||||
name="capacity"
|
||||
type="number"
|
||||
value={formData.capacity || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("capacity", parseInt(e.target.value) || 0)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="100"
|
||||
min={1}
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.capacity && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.capacity}</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Mevcut Stok
|
||||
</label>
|
||||
<input
|
||||
<FormItem
|
||||
label="Mevcut Stok"
|
||||
invalid={!!(errors.currentStock && touched.currentStock)}
|
||||
errorMessage={errors.currentStock}
|
||||
>
|
||||
<Field
|
||||
name="currentStock"
|
||||
type="number"
|
||||
value={formData.currentStock || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"currentStock",
|
||||
parseInt(e.target.value) || 0
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="0"
|
||||
max={formData.capacity || 0}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.currentStock && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.currentStock}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Dimensions */}
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Boyutlar
|
||||
</h3>
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">Boyutlar</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Uzunluk (m)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.dimensions?.length || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"dimensions.length",
|
||||
parseFloat(e.target.value) || 0
|
||||
{['length', 'width', 'height', 'weight'].map((dim) => (
|
||||
<FormItem
|
||||
key={dim}
|
||||
label={
|
||||
dim === 'length'
|
||||
? 'Uzunluk (m)'
|
||||
: dim === 'width'
|
||||
? 'Genişlik (m)'
|
||||
: dim === 'height'
|
||||
? 'Yükseklik (m)'
|
||||
: `Maks. Ağırlık (${values.dimensions?.unit})`
|
||||
}
|
||||
invalid={
|
||||
!!(
|
||||
getIn(errors, `dimensions.${dim}`) &&
|
||||
getIn(touched, `dimensions.${dim}`)
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="0"
|
||||
errorMessage={getIn(errors, `dimensions.${dim}`)}
|
||||
>
|
||||
<Field
|
||||
name={`dimensions.${dim}`}
|
||||
type="number"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="2.5"
|
||||
min={0}
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Genişlik (m)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.dimensions?.width || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"dimensions.width",
|
||||
parseFloat(e.target.value) || 0
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="0"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="1.2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Yükseklik (m)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.dimensions?.height || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"dimensions.height",
|
||||
parseFloat(e.target.value) || 0
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="0"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="3.0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Maks. Ağırlık ({formData.dimensions?.unit || "kg"})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.dimensions?.weight || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"dimensions.weight",
|
||||
parseFloat(e.target.value) || 0
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="0"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="1000"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Restrictions */}
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Kısıtlamalar
|
||||
</h3>
|
||||
{mode !== "view" && (
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">Kısıtlamalar</h3>
|
||||
{mode !== 'view' && (
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
<Input
|
||||
value={restrictionInput}
|
||||
onChange={(e) => setRestrictionInput(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
className="flex-1 px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addRestriction()
|
||||
}
|
||||
}}
|
||||
placeholder="Kısıtlama ekle..."
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
onClick={addRestriction}
|
||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
Ekle
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.restrictions?.map((restriction, index) => (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{values.restrictions?.map((restriction, index) => (
|
||||
<span
|
||||
key={index}
|
||||
key={`${restriction}-${index}`}
|
||||
className="inline-flex items-center px-2.5 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full"
|
||||
>
|
||||
{restriction}
|
||||
{mode !== "view" && (
|
||||
{mode !== 'view' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRestriction(index)}
|
||||
|
|
@ -525,29 +392,21 @@ const LocationModal: React.FC<LocationModalProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
<FormItem>
|
||||
<Field
|
||||
name="isActive"
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive || false}
|
||||
onChange={(e) => handleInputChange("isActive", e.target.checked)}
|
||||
disabled={mode === "view"}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
htmlFor="isActive"
|
||||
className="ml-2 block text-sm text-gray-900"
|
||||
component={Checkbox}
|
||||
disabled={mode === 'view'}
|
||||
>
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
{/* Info (in view mode) */}
|
||||
{mode === "view" && (
|
||||
{/* Info (View Mode) */}
|
||||
{mode === 'view' && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 space-y-2">
|
||||
<h4 className="text-sm font-medium text-gray-900">
|
||||
Lokasyon Bilgileri
|
||||
</h4>
|
||||
<h4 className="text-sm font-medium text-gray-900">Lokasyon Bilgileri</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
|
||||
{selectedWarehouse && (
|
||||
<div>
|
||||
|
|
@ -555,7 +414,7 @@ const LocationModal: React.FC<LocationModalProps> = ({
|
|||
<strong>Depo:</strong> {selectedWarehouse.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Depo Kodu:</strong> {selectedWarehouse.code}
|
||||
<strong>Kodu:</strong> {selectedWarehouse.code}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -565,21 +424,19 @@ const LocationModal: React.FC<LocationModalProps> = ({
|
|||
<strong>Bölge:</strong> {selectedZone.name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Bölge Kodu:</strong> {selectedZone.zoneCode}
|
||||
<strong>Kodu:</strong> {selectedZone.zoneCode}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p>
|
||||
<strong>Kapasite Kullanımı:</strong>{" "}
|
||||
{formData.currentStock || 0} / {formData.capacity || 0}
|
||||
<strong>Kapasite Kullanımı:</strong> {values.currentStock} /{' '}
|
||||
{values.capacity}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Doluluk Oranı:</strong> %
|
||||
{Math.round(
|
||||
((formData.currentStock || 0) /
|
||||
(formData.capacity || 1)) *
|
||||
100
|
||||
((values.currentStock || 0) / (values.capacity || 1)) * 100,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -589,27 +446,34 @@ const LocationModal: React.FC<LocationModalProps> = ({
|
|||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-1.5 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
variant="default"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{mode === "view" ? "Kapat" : "İptal"}
|
||||
</button>
|
||||
{mode !== "view" && (
|
||||
<button
|
||||
{mode === 'view' ? 'Kapat' : 'İptal'}
|
||||
</Button>
|
||||
{mode !== 'view' && (
|
||||
<Button
|
||||
type="submit"
|
||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
loading={isSubmitting}
|
||||
icon={<FaSave className="w-4 h-4" />}
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
{mode === "create" ? "Oluştur" : "Güncelle"}
|
||||
</button>
|
||||
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default LocationModal;
|
||||
export default LocationModal
|
||||
|
|
|
|||
|
|
@ -1,16 +1,37 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { WmWarehouse, WarehouseTypeEnum } from "../../../../types/wm";
|
||||
import { FaTimes, FaSave, FaBuilding } from "react-icons/fa";
|
||||
import { getWarehouseTypeText } from "../../../../utils/erp";
|
||||
import React from 'react'
|
||||
import { Formik, Form, Field, FieldProps, getIn } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { WmWarehouse, WarehouseTypeEnum } from '../../../../types/wm'
|
||||
import { FaTimes, FaSave, FaBuilding } from 'react-icons/fa'
|
||||
import { getWarehouseTypeText } from '../../../../utils/erp'
|
||||
|
||||
import { Input, Checkbox, FormContainer, FormItem } from '@/components/ui'
|
||||
import Select from '@/components/ui/Select'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface WarehouseModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (warehouse: Partial<WmWarehouse>) => void;
|
||||
warehouse?: WmWarehouse | null;
|
||||
mode: "create" | "edit" | "view";
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (warehouse: Partial<WmWarehouse>) => void
|
||||
warehouse?: WmWarehouse | null
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
code: Yup.string().required('Depo kodu zorunludur'),
|
||||
name: Yup.string().required('Depo adı zorunludur'),
|
||||
capacity: Yup.number()
|
||||
.moreThan(0, "Kapasite 0'dan büyük olmalıdır")
|
||||
.required('Kapasite zorunludur'),
|
||||
address: Yup.object({
|
||||
street: Yup.string().required('Adres zorunludur'),
|
||||
city: Yup.string().required('Şehir zorunludur'),
|
||||
state: Yup.string().nullable(),
|
||||
postalCode: Yup.string().nullable(),
|
||||
country: Yup.string().required('Ülke zorunludur'),
|
||||
}),
|
||||
})
|
||||
|
||||
const WarehouseModal: React.FC<WarehouseModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
|
@ -18,123 +39,38 @@ const WarehouseModal: React.FC<WarehouseModalProps> = ({
|
|||
warehouse,
|
||||
mode,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Partial<WmWarehouse>>({
|
||||
code: "",
|
||||
name: "",
|
||||
description: "",
|
||||
if (!isOpen) return null
|
||||
|
||||
const initialValues: Partial<WmWarehouse> = {
|
||||
code: warehouse?.code || '',
|
||||
name: warehouse?.name || '',
|
||||
description: warehouse?.description || '',
|
||||
address: {
|
||||
street: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "Türkiye",
|
||||
street: warehouse?.address?.street || '',
|
||||
city: warehouse?.address?.city || '',
|
||||
state: warehouse?.address?.state || '',
|
||||
postalCode: warehouse?.address?.postalCode || '',
|
||||
country: warehouse?.address?.country || 'Türkiye',
|
||||
},
|
||||
warehouseType: WarehouseTypeEnum.RawMaterials,
|
||||
isMainWarehouse: false,
|
||||
capacity: 0,
|
||||
currentUtilization: 0,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (warehouse) {
|
||||
setFormData(warehouse);
|
||||
} else {
|
||||
setFormData({
|
||||
code: "",
|
||||
name: "",
|
||||
description: "",
|
||||
address: {
|
||||
street: "",
|
||||
city: "",
|
||||
state: "",
|
||||
postalCode: "",
|
||||
country: "Türkiye",
|
||||
},
|
||||
warehouseType: WarehouseTypeEnum.RawMaterials,
|
||||
isMainWarehouse: false,
|
||||
capacity: 0,
|
||||
currentUtilization: 0,
|
||||
isActive: true,
|
||||
});
|
||||
warehouseType: warehouse?.warehouseType || WarehouseTypeEnum.RawMaterials,
|
||||
isMainWarehouse: warehouse?.isMainWarehouse ?? false,
|
||||
capacity: warehouse?.capacity || 0,
|
||||
currentUtilization: warehouse?.currentUtilization || 0,
|
||||
isActive: warehouse?.isActive ?? true,
|
||||
}
|
||||
setErrors({});
|
||||
}, [warehouse, isOpen]);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.code?.trim()) {
|
||||
newErrors.code = "Depo kodu zorunludur";
|
||||
}
|
||||
if (!formData.name?.trim()) {
|
||||
newErrors.name = "Depo adı zorunludur";
|
||||
}
|
||||
if (!formData.address?.street?.trim()) {
|
||||
newErrors.street = "Adres zorunludur";
|
||||
}
|
||||
if (!formData.address?.city?.trim()) {
|
||||
newErrors.city = "Şehir zorunludur";
|
||||
}
|
||||
if (!formData.capacity || formData.capacity <= 0) {
|
||||
newErrors.capacity = "Kapasite 0'dan büyük olmalıdır";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (mode === "view") return;
|
||||
|
||||
if (validateForm()) {
|
||||
onSave(formData);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
if (field.includes(".")) {
|
||||
const [parent, child] = field.split(".");
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[parent]: {
|
||||
...(prev[parent as keyof typeof prev] as object),
|
||||
[child]: value,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
}
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const getModalTitle = () => {
|
||||
switch (mode) {
|
||||
case "create":
|
||||
return "Yeni Depo Oluştur";
|
||||
case "edit":
|
||||
return "Depo Düzenle";
|
||||
case "view":
|
||||
return "Depo Görüntüle";
|
||||
case 'create':
|
||||
return 'Yeni Depo Oluştur'
|
||||
case 'edit':
|
||||
return 'Depo Düzenle'
|
||||
case 'view':
|
||||
return 'Depo Görüntüle'
|
||||
default:
|
||||
return "Depo";
|
||||
return 'Depo'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
|
|
@ -145,275 +81,226 @@ const WarehouseModal: React.FC<WarehouseModalProps> = ({
|
|||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<FaBuilding className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{getModalTitle()}
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">{getModalTitle()}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
||||
{/* Basic Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo Kodu *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.code || ""}
|
||||
onChange={(e) => handleInputChange("code", e.target.value)}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="WH001"
|
||||
/>
|
||||
{errors.warehouseCode && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.warehouseCode}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name || ""}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="Ana Depo"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Açıklama
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
disabled={mode === "view"}
|
||||
rows={3}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="Depo açıklaması..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Warehouse Type and Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo Tipi
|
||||
</label>
|
||||
<select
|
||||
value={formData.warehouseType || WarehouseTypeEnum.RawMaterials}
|
||||
onChange={(e) =>
|
||||
handleInputChange("warehouseType", e.target.value)
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, helpers) => {
|
||||
if (mode !== 'view') {
|
||||
console.log('Submitting values:', values)
|
||||
onSave(values)
|
||||
onClose()
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
helpers.setSubmitting(false)
|
||||
}}
|
||||
>
|
||||
{Object.values(WarehouseTypeEnum).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getWarehouseTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kapasite *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.capacity || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("capacity", parseInt(e.target.value) || 0)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="1000"
|
||||
/>
|
||||
{errors.capacity && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.capacity}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{({ errors, touched, isSubmitting, values }) => (
|
||||
<Form className="p-4 space-y-4">
|
||||
<FormContainer size="sm">
|
||||
{/* Basic Info */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isMainWarehouse"
|
||||
checked={formData.isMainWarehouse || false}
|
||||
onChange={(e) =>
|
||||
handleInputChange("isMainWarehouse", e.target.checked)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
<FormItem
|
||||
label="Depo Kodu *"
|
||||
invalid={!!(errors.code && touched.code)}
|
||||
errorMessage={errors.code}
|
||||
>
|
||||
<Field
|
||||
name="code"
|
||||
placeholder="WH001"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
<label
|
||||
htmlFor="isMainWarehouse"
|
||||
className="ml-2 block text-sm text-gray-900"
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Depo Adı *"
|
||||
invalid={!!(errors.name && touched.name)}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Ana Depo"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<FormItem label="Açıklama">
|
||||
<Field
|
||||
name="description"
|
||||
as="textarea"
|
||||
rows={3}
|
||||
placeholder="Depo açıklaması..."
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
{/* Warehouse Type + Capacity */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem label="Depo Tipi">
|
||||
<Field name="warehouseType">
|
||||
{({ field, form }: FieldProps<WarehouseTypeEnum>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={false}
|
||||
isDisabled={mode === 'view'}
|
||||
options={Object.values(WarehouseTypeEnum).map((t) => ({
|
||||
value: t,
|
||||
label: getWarehouseTypeText(t),
|
||||
}))}
|
||||
value={Object.values(WarehouseTypeEnum)
|
||||
.map((t) => ({
|
||||
value: t,
|
||||
label: getWarehouseTypeText(t),
|
||||
}))
|
||||
.find((opt) => opt.value === values.warehouseType)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Kapasite *"
|
||||
invalid={!!(errors.capacity && touched.capacity)}
|
||||
errorMessage={errors.capacity}
|
||||
>
|
||||
<Field
|
||||
name="capacity"
|
||||
type="number"
|
||||
placeholder="1000"
|
||||
min={1}
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem>
|
||||
<Field
|
||||
name="isMainWarehouse"
|
||||
type="checkbox"
|
||||
component={Checkbox}
|
||||
disabled={mode === 'view'}
|
||||
>
|
||||
Ana Depo
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
<FormItem>
|
||||
<Field
|
||||
name="isActive"
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive || false}
|
||||
onChange={(e) =>
|
||||
handleInputChange("isActive", e.target.checked)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
htmlFor="isActive"
|
||||
className="ml-2 block text-sm text-gray-900"
|
||||
component={Checkbox}
|
||||
disabled={mode === 'view'}
|
||||
>
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Address Information */}
|
||||
{/* Address */}
|
||||
<div>
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Adres Bilgileri
|
||||
</h3>
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">Adres Bilgileri</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Adres *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.street || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("address.street", e.target.value)
|
||||
<FormItem
|
||||
label="Adres *"
|
||||
invalid={
|
||||
!!(getIn(errors, 'address.street') && getIn(touched, 'address.street'))
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
errorMessage={getIn(errors, 'address.street')}
|
||||
>
|
||||
<Field
|
||||
name="address.street"
|
||||
placeholder="Cadde/Sokak ve Bina No"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.street && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.street}</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Şehir *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.city || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("address.city", e.target.value)
|
||||
<FormItem
|
||||
label="Şehir *"
|
||||
invalid={
|
||||
!!(getIn(errors, 'address.city') && getIn(touched, 'address.city'))
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
errorMessage={getIn(errors, 'address.city')}
|
||||
>
|
||||
<Field
|
||||
name="address.city"
|
||||
placeholder="İstanbul"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.city && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.city}</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
İl
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.state || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("address.state", e.target.value)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<FormItem label="İlçe">
|
||||
<Field
|
||||
name="address.state"
|
||||
placeholder="İstanbul"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Posta Kodu
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.postalCode || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("address.postalCode", e.target.value)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<FormItem label="Posta Kodu">
|
||||
<Field
|
||||
name="address.postalCode"
|
||||
placeholder="34000"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Ülke
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.address?.country || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange("address.country", e.target.value)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<FormItem label="Ülke">
|
||||
<Field
|
||||
name="address.country"
|
||||
placeholder="Türkiye"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-1.5 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
{mode === "view" ? "Kapat" : "İptal"}
|
||||
</button>
|
||||
{mode !== "view" && (
|
||||
<button
|
||||
<Button type="button" onClick={onClose} variant="default" disabled={isSubmitting}>
|
||||
{mode === 'view' ? 'Kapat' : 'İptal'}
|
||||
</Button>
|
||||
{mode !== 'view' && (
|
||||
<Button
|
||||
type="submit"
|
||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
loading={isSubmitting}
|
||||
icon={<FaSave className="w-4 h-4" />}
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
{mode === "create" ? "Oluştur" : "Güncelle"}
|
||||
</button>
|
||||
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default WarehouseModal;
|
||||
export default WarehouseModal
|
||||
|
|
|
|||
|
|
@ -1,17 +1,38 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { WmZone, ZoneTypeEnum, WmWarehouse } from "../../../../types/wm";
|
||||
import { FaTimes, FaSave, FaLayerGroup } from "react-icons/fa";
|
||||
import { getZoneTypeText } from "../../../../utils/erp";
|
||||
import React from 'react'
|
||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { WmZone, ZoneTypeEnum, WmWarehouse } from '../../../../types/wm'
|
||||
import { FaTimes, FaSave, FaLayerGroup } from 'react-icons/fa'
|
||||
import { getZoneTypeText } from '../../../../utils/erp'
|
||||
|
||||
// Senin UI bileşenlerin
|
||||
import { Input, Checkbox, FormContainer, FormItem } from '@/components/ui'
|
||||
import Select from '@/components/ui/Select'
|
||||
import Button from '@/components/ui/Button'
|
||||
|
||||
interface ZoneModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (zone: Partial<WmZone>) => void;
|
||||
zone?: WmZone | null;
|
||||
mode: "create" | "edit" | "view";
|
||||
warehouses: WmWarehouse[];
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (zone: Partial<WmZone>) => void
|
||||
zone?: WmZone | null
|
||||
mode: 'create' | 'edit' | 'view'
|
||||
warehouses: WmWarehouse[]
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
warehouseId: Yup.string().required('Depo seçimi zorunludur'),
|
||||
zoneCode: Yup.string().required('Bölge kodu zorunludur'),
|
||||
name: Yup.string().required('Bölge adı zorunludur'),
|
||||
temperature: Yup.number()
|
||||
.min(-50, 'Sıcaklık -50°C’den küçük olamaz')
|
||||
.max(100, 'Sıcaklık 100°C’den büyük olamaz')
|
||||
.nullable(),
|
||||
humidity: Yup.number()
|
||||
.min(0, 'Nem %0’dan küçük olamaz')
|
||||
.max(100, 'Nem %100’den büyük olamaz')
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
const ZoneModal: React.FC<ZoneModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
|
@ -20,108 +41,33 @@ const ZoneModal: React.FC<ZoneModalProps> = ({
|
|||
mode,
|
||||
warehouses,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<Partial<WmZone>>({
|
||||
warehouseId: "",
|
||||
zoneCode: "",
|
||||
name: "",
|
||||
description: "",
|
||||
zoneType: ZoneTypeEnum.Storage,
|
||||
temperature: undefined,
|
||||
humidity: undefined,
|
||||
isActive: true,
|
||||
});
|
||||
if (!isOpen) return null
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (zone) {
|
||||
setFormData(zone);
|
||||
} else {
|
||||
setFormData({
|
||||
warehouseId: "",
|
||||
zoneCode: "",
|
||||
name: "",
|
||||
description: "",
|
||||
zoneType: ZoneTypeEnum.Storage,
|
||||
temperature: undefined,
|
||||
humidity: undefined,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
setErrors({});
|
||||
}, [zone, isOpen]);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.warehouseId?.trim()) {
|
||||
newErrors.warehouseId = "Depo seçimi zorunludur";
|
||||
}
|
||||
if (!formData.zoneCode?.trim()) {
|
||||
newErrors.zoneCode = "Bölge kodu zorunludur";
|
||||
}
|
||||
if (!formData.name?.trim()) {
|
||||
newErrors.name = "Bölge adı zorunludur";
|
||||
}
|
||||
if (
|
||||
formData.temperature &&
|
||||
(formData.temperature < -50 || formData.temperature > 100)
|
||||
) {
|
||||
newErrors.temperature = "Sıcaklık -50°C ile 100°C arasında olmalıdır";
|
||||
}
|
||||
if (
|
||||
formData.humidity &&
|
||||
(formData.humidity < 0 || formData.humidity > 100)
|
||||
) {
|
||||
newErrors.humidity = "Nem %0 ile %100 arasında olmalıdır";
|
||||
const initialValues: Partial<WmZone> = {
|
||||
warehouseId: zone?.warehouseId || '',
|
||||
zoneCode: zone?.zoneCode || '',
|
||||
name: zone?.name || '',
|
||||
description: zone?.description || '',
|
||||
zoneType: zone?.zoneType || ZoneTypeEnum.Storage,
|
||||
temperature: zone?.temperature,
|
||||
humidity: zone?.humidity,
|
||||
isActive: zone?.isActive ?? true,
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (mode === "view") return;
|
||||
|
||||
if (validateForm()) {
|
||||
onSave(formData);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | boolean | undefined
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||
}
|
||||
};
|
||||
const selectedWarehouse = warehouses.find((w) => w.id === initialValues.warehouseId)
|
||||
|
||||
const getModalTitle = () => {
|
||||
switch (mode) {
|
||||
case "create":
|
||||
return "Yeni Bölge Oluştur";
|
||||
case "edit":
|
||||
return "Bölge Düzenle";
|
||||
case "view":
|
||||
return "Bölge Görüntüle";
|
||||
case 'create':
|
||||
return 'Yeni Bölge Oluştur'
|
||||
case 'edit':
|
||||
return 'Bölge Düzenle'
|
||||
case 'view':
|
||||
return 'Bölge Görüntüle'
|
||||
default:
|
||||
return "Bölge";
|
||||
return 'Bölge'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const selectedWarehouse = warehouses.find(
|
||||
(w) => w.id === formData.warehouseId
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
|
|
@ -132,194 +78,177 @@ const ZoneModal: React.FC<ZoneModalProps> = ({
|
|||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<FaLayerGroup className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{getModalTitle()}
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900">{getModalTitle()}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-4 space-y-4">
|
||||
{/* Warehouse Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Depo *
|
||||
</label>
|
||||
<select
|
||||
value={formData.warehouseId || ""}
|
||||
onChange={(e) => handleInputChange("warehouseId", e.target.value)}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values, helpers) => {
|
||||
if (mode !== 'view') {
|
||||
console.log('Submitting values:', values)
|
||||
onSave(values)
|
||||
onClose()
|
||||
}
|
||||
helpers.setSubmitting(false)
|
||||
}}
|
||||
>
|
||||
<option value="">Depo seçin</option>
|
||||
{warehouses.map((warehouse) => (
|
||||
<option key={warehouse.id} value={warehouse.id}>
|
||||
{warehouse.name} ({warehouse.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.warehouseId && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.warehouseId}</p>
|
||||
{({ errors, touched, isSubmitting, values }) => (
|
||||
<Form className="p-4 space-y-4">
|
||||
<FormContainer size="sm">
|
||||
{/* Warehouse */}
|
||||
<FormItem
|
||||
label="Depo *"
|
||||
invalid={!!(errors.warehouseId && touched.warehouseId)}
|
||||
errorMessage={errors.warehouseId}
|
||||
>
|
||||
<Field name="warehouseId">
|
||||
{({ field, form }: FieldProps<string>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={true}
|
||||
isDisabled={mode === 'view'}
|
||||
options={warehouses.map((w) => ({
|
||||
value: w.id,
|
||||
label: `${w.name} (${w.code})`,
|
||||
}))}
|
||||
value={warehouses
|
||||
.map((w) => ({
|
||||
value: w.id,
|
||||
label: `${w.name} (${w.code})`,
|
||||
}))
|
||||
.find((opt) => opt.value === values.warehouseId)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
{/* Basic Information */}
|
||||
{/* Zone Code + Zone Name */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bölge Kodu *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.zoneCode || ""}
|
||||
onChange={(e) => handleInputChange("zoneCode", e.target.value)}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<FormItem
|
||||
label="Bölge Kodu *"
|
||||
invalid={!!(errors.zoneCode && touched.zoneCode)}
|
||||
errorMessage={errors.zoneCode}
|
||||
>
|
||||
<Field
|
||||
name="zoneCode"
|
||||
placeholder="Z001"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.zoneCode && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.zoneCode}</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bölge Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name || ""}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
disabled={mode === "view" || mode === "edit"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
<FormItem
|
||||
label="Bölge Adı *"
|
||||
invalid={!!(errors.name && touched.name)}
|
||||
errorMessage={errors.name}
|
||||
>
|
||||
<Field
|
||||
name="name"
|
||||
placeholder="Ana Depolama"
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Açıklama
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
disabled={mode === "view"}
|
||||
{/* Description */}
|
||||
<FormItem label="Açıklama">
|
||||
<Field
|
||||
name="description"
|
||||
as="textarea"
|
||||
rows={3}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="Bölge açıklaması..."
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
{/* Zone Type */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bölge Tipi
|
||||
</label>
|
||||
<select
|
||||
value={formData.zoneType || ZoneTypeEnum.Storage}
|
||||
onChange={(e) =>
|
||||
handleInputChange("zoneType", e.target.value as ZoneTypeEnum)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
>
|
||||
{Object.values(ZoneTypeEnum).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getZoneTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<FormItem label="Bölge Tipi">
|
||||
<Field name="zoneType">
|
||||
{({ field, form }: FieldProps<ZoneTypeEnum>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
isClearable={false}
|
||||
isDisabled={mode === 'view'}
|
||||
options={Object.values(ZoneTypeEnum).map((t) => ({
|
||||
value: t,
|
||||
label: getZoneTypeText(t),
|
||||
}))}
|
||||
value={Object.values(ZoneTypeEnum)
|
||||
.map((t) => ({
|
||||
value: t,
|
||||
label: getZoneTypeText(t),
|
||||
}))
|
||||
.find((opt) => opt.value === values.zoneType)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
{/* Environmental Conditions */}
|
||||
{/* Temperature + Humidity */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sıcaklık (°C)
|
||||
</label>
|
||||
<input
|
||||
<FormItem
|
||||
label="Sıcaklık (°C)"
|
||||
invalid={!!(errors.temperature && touched.temperature)}
|
||||
errorMessage={errors.temperature}
|
||||
>
|
||||
<Field
|
||||
name="temperature"
|
||||
type="number"
|
||||
value={formData.temperature || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"temperature",
|
||||
e.target.value ? parseFloat(e.target.value) : undefined
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="-50"
|
||||
max="100"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="20"
|
||||
min={-50}
|
||||
max={100}
|
||||
step={0.1}
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.temperature && (
|
||||
<p className="text-red-500 text-sm mt-1">
|
||||
{errors.temperature}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nem (%)
|
||||
</label>
|
||||
<input
|
||||
<FormItem
|
||||
label="Nem (%)"
|
||||
invalid={!!(errors.humidity && touched.humidity)}
|
||||
errorMessage={errors.humidity}
|
||||
>
|
||||
<Field
|
||||
name="humidity"
|
||||
type="number"
|
||||
value={formData.humidity || ""}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
"humidity",
|
||||
e.target.value ? parseFloat(e.target.value) : undefined
|
||||
)
|
||||
}
|
||||
disabled={mode === "view"}
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100"
|
||||
placeholder="45"
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.1}
|
||||
disabled={mode === 'view'}
|
||||
component={Input}
|
||||
/>
|
||||
{errors.humidity && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.humidity}</p>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
<FormItem>
|
||||
<Field
|
||||
name="isActive"
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive || false}
|
||||
onChange={(e) => handleInputChange("isActive", e.target.checked)}
|
||||
disabled={mode === "view"}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label
|
||||
htmlFor="isActive"
|
||||
className="ml-2 block text-sm text-gray-900"
|
||||
component={Checkbox}
|
||||
disabled={mode === 'view'}
|
||||
>
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
{/* Warehouse Info (in view mode) */}
|
||||
{mode === "view" && selectedWarehouse && (
|
||||
{/* Warehouse Info (view mode) */}
|
||||
{mode === 'view' && selectedWarehouse && (
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">
|
||||
Depo Bilgileri
|
||||
</h4>
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">Depo Bilgileri</h4>
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>
|
||||
<strong>Depo:</strong> {selectedWarehouse.name}
|
||||
|
|
@ -328,7 +257,7 @@ const ZoneModal: React.FC<ZoneModalProps> = ({
|
|||
<strong>Kod:</strong> {selectedWarehouse.code}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Adres:</strong> {selectedWarehouse.address?.city},{" "}
|
||||
<strong>Adres:</strong> {selectedWarehouse.address?.city},{' '}
|
||||
{selectedWarehouse.address?.state}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -337,27 +266,28 @@ const ZoneModal: React.FC<ZoneModalProps> = ({
|
|||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-1.5 text-sm text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
>
|
||||
{mode === "view" ? "Kapat" : "İptal"}
|
||||
</button>
|
||||
{mode !== "view" && (
|
||||
<button
|
||||
<Button type="button" onClick={onClose} variant="default" disabled={isSubmitting}>
|
||||
{mode === 'view' ? 'Kapat' : 'İptal'}
|
||||
</Button>
|
||||
{mode !== 'view' && (
|
||||
<Button
|
||||
type="submit"
|
||||
className="px-4 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center gap-2"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
loading={isSubmitting}
|
||||
icon={<FaSave className="w-4 h-4" />}
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
{mode === "create" ? "Oluştur" : "Güncelle"}
|
||||
</button>
|
||||
{mode === 'create' ? 'Oluştur' : 'Güncelle'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</FormContainer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default ZoneModal;
|
||||
export default ZoneModal
|
||||
|
|
|
|||
Loading…
Reference in a new issue