387 lines
15 KiB
TypeScript
387 lines
15 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
||
import { FaTimes, FaSave, FaUsers, FaPlus, FaTrash } from 'react-icons/fa'
|
||
import {
|
||
MmApprovalWorkflow,
|
||
MmApprovalWorkflowLevel,
|
||
RequestTypeEnum,
|
||
ApprovalLevelEnum,
|
||
} from '../../../types/mm'
|
||
import MultiSelectEmployee from '../../../components/common/MultiSelectEmployee'
|
||
import { mockDepartments } from '../../../mocks/mockDepartments'
|
||
|
||
interface ApprovalWorkflowModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
onSave: (workflow: MmApprovalWorkflow) => void
|
||
workflow?: MmApprovalWorkflow | null
|
||
mode: 'create' | 'view' | 'edit'
|
||
}
|
||
|
||
const ApprovalWorkflowModal: React.FC<ApprovalWorkflowModalProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
onSave,
|
||
workflow,
|
||
mode,
|
||
}) => {
|
||
const [formData, setFormData] = useState<Partial<MmApprovalWorkflow>>({
|
||
name: '',
|
||
departmentId: '',
|
||
requestType: RequestTypeEnum.Material,
|
||
amountThreshold: 0,
|
||
approvalLevels: [],
|
||
isActive: true,
|
||
})
|
||
|
||
useEffect(() => {
|
||
if (mode === 'create') {
|
||
// Reset form to initial empty state for new workflow
|
||
setFormData({
|
||
name: '',
|
||
departmentId: '',
|
||
requestType: RequestTypeEnum.Material,
|
||
amountThreshold: 0,
|
||
approvalLevels: [],
|
||
isActive: true,
|
||
})
|
||
} else if (workflow) {
|
||
// Load existing workflow data for view/edit modes
|
||
setFormData(workflow)
|
||
}
|
||
}, [workflow, mode])
|
||
|
||
const handleInputChange = (
|
||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
|
||
) => {
|
||
const { name, value, type } = e.target
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
[name]:
|
||
type === 'number'
|
||
? parseFloat(value) || 0
|
||
: type === 'checkbox'
|
||
? (e.target as HTMLInputElement).checked
|
||
: value,
|
||
}))
|
||
}
|
||
|
||
const addApprovalLevel = () => {
|
||
const newLevel: MmApprovalWorkflowLevel = {
|
||
id: `level-${Date.now()}`,
|
||
workflowId: formData.id || '',
|
||
level: ApprovalLevelEnum.Supervisor,
|
||
approverUserIds: [],
|
||
approverNames: [],
|
||
sequence: (formData.approvalLevels?.length || 0) + 1,
|
||
isRequired: true,
|
||
isParallel: false,
|
||
}
|
||
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
approvalLevels: [...(prev.approvalLevels || []), newLevel],
|
||
}))
|
||
}
|
||
|
||
const removeApprovalLevel = (index: number) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
approvalLevels: prev.approvalLevels?.filter((_, i) => i !== index) || [],
|
||
}))
|
||
}
|
||
|
||
const updateApprovalLevel = (
|
||
index: number,
|
||
field: keyof MmApprovalWorkflowLevel,
|
||
value: string | number | boolean | string[] | undefined,
|
||
) => {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
approvalLevels:
|
||
prev.approvalLevels?.map((level, i) =>
|
||
i === index ? { ...level, [field]: value } : level,
|
||
) || [],
|
||
}))
|
||
}
|
||
|
||
const handleApproverNamesChange = (index: number, employees: string[]) => {
|
||
updateApprovalLevel(index, 'approverNames', employees)
|
||
}
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (mode !== 'view') {
|
||
const newWorkflow: MmApprovalWorkflow = {
|
||
id: workflow?.id || `WF-${Date.now()}`,
|
||
name: formData.name || '',
|
||
departmentId: formData.departmentId || '',
|
||
requestType: formData.requestType || RequestTypeEnum.Material,
|
||
amountThreshold: formData.amountThreshold || 0,
|
||
approvalLevels: formData.approvalLevels || [],
|
||
isActive: formData.isActive ?? true,
|
||
creationTime: workflow?.creationTime || new Date(),
|
||
lastModificationTime: new Date(),
|
||
}
|
||
onSave(newWorkflow)
|
||
}
|
||
onClose()
|
||
}
|
||
|
||
if (!isOpen) return null
|
||
|
||
const isReadOnly = mode === 'view'
|
||
const modalTitle =
|
||
mode === 'create'
|
||
? 'Yeni Onay Süreci'
|
||
: mode === 'edit'
|
||
? 'Onay Sürecini Düzenle'
|
||
: 'Onay Süreci Detayları'
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||
<div className="relative top-5 mx-auto p-4 border w-11/12 max-w-5xl shadow-lg rounded-md bg-white">
|
||
<div className="flex items-center justify-between pb-2 border-b">
|
||
<h3 className="text-base font-semibold text-gray-900 flex items-center">
|
||
<FaUsers className="mr-2 text-blue-600" />
|
||
{modalTitle}
|
||
</h3>
|
||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1">
|
||
<FaTimes />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="mt-2">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Temel Bilgiler */}
|
||
<div className="space-y-3">
|
||
<h4 className="text-base font-medium text-gray-900 border-b pb-1">Temel Bilgiler</h4>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">Süreç Adı</label>
|
||
<input
|
||
type="text"
|
||
name="workflowName"
|
||
value={formData.name || ''}
|
||
onChange={handleInputChange}
|
||
readOnly={isReadOnly}
|
||
className="mt-1 block w-full border border-gray-300 rounded-md px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">Departman</label>
|
||
<select
|
||
value={formData.departmentId || ''}
|
||
onChange={handleInputChange}
|
||
className="mt-1 block w-full border border-gray-300 rounded-md px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
required
|
||
>
|
||
<option value="">Departman Seçin...</option>
|
||
{mockDepartments.map((dept) => (
|
||
<option key={dept.id} value={dept.id}>
|
||
{dept.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">Talep Tipi</label>
|
||
<select
|
||
name="requestType"
|
||
value={formData.requestType || RequestTypeEnum.Material}
|
||
onChange={handleInputChange}
|
||
disabled={isReadOnly}
|
||
className="mt-1 block w-full border border-gray-300 rounded-md px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
>
|
||
<option value={RequestTypeEnum.Material}>Malzeme</option>
|
||
<option value={RequestTypeEnum.Service}>Hizmet</option>
|
||
<option value={RequestTypeEnum.WorkCenter}>İş Merkezi</option>
|
||
<option value={RequestTypeEnum.Maintenance}>Bakım</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">Tutar Eşiği (TL)</label>
|
||
<input
|
||
type="number"
|
||
name="amountThreshold"
|
||
value={formData.amountThreshold || 0}
|
||
onChange={handleInputChange}
|
||
readOnly={isReadOnly}
|
||
className="mt-1 block w-full border border-gray-300 rounded-md px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
name="isActive"
|
||
checked={formData.isActive || false}
|
||
onChange={handleInputChange}
|
||
disabled={isReadOnly}
|
||
className="mr-2"
|
||
/>
|
||
<span className="text-sm font-medium text-gray-700">Aktif</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Onay Seviyeleri */}
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<h4 className="text-base font-medium text-gray-900 border-b pb-1">
|
||
Onay Seviyeleri
|
||
</h4>
|
||
{!isReadOnly && (
|
||
<button
|
||
type="button"
|
||
onClick={addApprovalLevel}
|
||
className="px-3 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center text-sm"
|
||
>
|
||
<FaPlus className="mr-1" />
|
||
Seviye Ekle
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2 max-h-56 overflow-y-auto p-1">
|
||
{formData.approvalLevels?.map((level, index) => (
|
||
<div key={level.id} className="border rounded-lg p-2 bg-gray-50">
|
||
<div className="flex items-center justify-between mb-1">
|
||
<span className="text-sm font-medium text-gray-700">
|
||
Seviye {level.sequence}
|
||
</span>
|
||
{!isReadOnly && (
|
||
<button
|
||
type="button"
|
||
onClick={() => removeApprovalLevel(index)}
|
||
className="text-red-600 hover:text-red-800"
|
||
>
|
||
<FaTrash />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-1">
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-600">
|
||
Onay Seviyesi
|
||
</label>
|
||
<select
|
||
value={level.level}
|
||
onChange={(e) => updateApprovalLevel(index, 'level', e.target.value)}
|
||
disabled={isReadOnly}
|
||
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
>
|
||
<option value={ApprovalLevelEnum.Supervisor}>Süpervizör</option>
|
||
<option value={ApprovalLevelEnum.Manager}>Müdür</option>
|
||
<option value={ApprovalLevelEnum.Director}>Direktör</option>
|
||
<option value={ApprovalLevelEnum.GeneralManager}>Genel Müdür</option>
|
||
<option value={ApprovalLevelEnum.FinanceManager}>
|
||
Mali İşler Müdürü
|
||
</option>
|
||
<option value={ApprovalLevelEnum.TechnicalManager}>Teknik Müdür</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-600">
|
||
Onaylayıcılar
|
||
</label>
|
||
<MultiSelectEmployee
|
||
selectedEmployees={level.approverNames || []}
|
||
onChange={(employees) => handleApproverNamesChange(index, employees)}
|
||
disabled={isReadOnly}
|
||
placeholder="Onaylayıcı seçin..."
|
||
className="mt-1"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-xs font-medium text-gray-600">
|
||
Timeout (Gün)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={level.timeoutDays || ''}
|
||
onChange={(e) =>
|
||
updateApprovalLevel(
|
||
index,
|
||
'timeoutDays',
|
||
parseInt(e.target.value) || undefined,
|
||
)
|
||
}
|
||
readOnly={isReadOnly}
|
||
min="1"
|
||
className="mt-1 block w-full text-sm border border-gray-300 rounded-md px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex space-x-3">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={level.isRequired}
|
||
onChange={(e) =>
|
||
updateApprovalLevel(index, 'isRequired', e.target.checked)
|
||
}
|
||
disabled={isReadOnly}
|
||
className="mr-1"
|
||
/>
|
||
<span className="text-xs text-gray-600">Zorunlu</span>
|
||
</label>
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={level.isParallel}
|
||
onChange={(e) =>
|
||
updateApprovalLevel(index, 'isParallel', e.target.checked)
|
||
}
|
||
disabled={isReadOnly}
|
||
className="mr-1"
|
||
/>
|
||
<span className="text-xs text-gray-600">Paralel</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{formData.approvalLevels?.length === 0 && (
|
||
<div className="text-center text-gray-500 text-sm py-2">
|
||
Henüz onay seviyesi eklenmedi
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex justify-end space-x-3 mt-4 pt-3 border-t">
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="px-3 py-1.5 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50"
|
||
>
|
||
{mode === 'view' ? 'Kapat' : 'İptal'}
|
||
</button>
|
||
{mode !== 'view' && (
|
||
<button
|
||
type="submit"
|
||
className="px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 flex items-center text-sm"
|
||
>
|
||
<FaSave className="mr-2" />
|
||
Kaydet
|
||
</button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default ApprovalWorkflowModal
|