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

424 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useState } from 'react'
import { FaTimes, FaSave, FaUpload, FaMinus } from 'react-icons/fa'
import {
PmFaultNotification,
FaultTypeEnum,
CriticalityLevelEnum,
NotificationStatusEnum,
} from '../../../types/pm'
import { mockWorkCenters } from '../../../mocks/mockWorkCenters'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { PriorityEnum } from '../../../types/common'
import { getCriticalityLevelText, getFaultTypeText, getPriorityText } from '@/utils/erp'
interface NewFaultNotificationModalProps {
isOpen: boolean
onClose: () => void
onSave: (notification: Partial<PmFaultNotification>) => void
}
const NewFaultNotificationModal: React.FC<NewFaultNotificationModalProps> = ({
isOpen,
onClose,
onSave,
}) => {
const [notificationData, setNotificationData] = useState<Partial<PmFaultNotification>>({
notificationCode: `ARZ-${new Date().getFullYear()}-${String(Date.now()).slice(-3)}`,
faultType: FaultTypeEnum.Mechanical,
priority: PriorityEnum.Normal,
severity: CriticalityLevelEnum.Medium,
status: NotificationStatusEnum.Open,
followUpRequired: false,
isActive: true,
images: [],
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [uploadedImages, setUploadedImages] = useState<string[]>([])
if (!isOpen) return null
const validateForm = () => {
const newErrors: Record<string, string> = {}
if (!notificationData.workCenterId) {
newErrors.workCenterId = 'İş merkezi seçimi gerekli'
}
if (!notificationData.title?.trim()) {
newErrors.title = 'Başlık gerekli'
}
if (!notificationData.description?.trim()) {
newErrors.description = 'Açıklama gerekli'
}
if (!notificationData.location?.trim()) {
newErrors.location = 'Konum gerekli'
}
if (!notificationData.reportedBy?.trim()) {
newErrors.reportedBy = 'Bildiren kişi gerekli'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleInputChange = (
field: keyof PmFaultNotification,
value: string | number | boolean | undefined,
) => {
setNotificationData((prev) => ({
...prev,
[field]: value,
}))
// Clear error when user starts typing
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: '',
}))
}
}
const handleWorkCenterChange = (workCenterId: string) => {
const selectedWorkCenter = mockWorkCenters.find((eq) => eq.id === workCenterId)
if (selectedWorkCenter) {
setNotificationData((prev) => ({
...prev,
workCenterId: selectedWorkCenter.id,
workCenterCode: selectedWorkCenter.code,
workCenterName: selectedWorkCenter.name,
location: selectedWorkCenter.location,
}))
}
}
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files
if (files) {
const imageNames = Array.from(files).map((file) => file.name)
setUploadedImages((prev) => [...prev, ...imageNames])
setNotificationData((prev) => ({
...prev,
images: [...(prev.images || []), ...imageNames],
}))
}
}
const removeImage = (index: number) => {
setUploadedImages((prev) => prev.filter((_, i) => i !== index))
setNotificationData((prev) => ({
...prev,
images: prev.images?.filter((_, i) => i !== index) || [],
}))
}
const handleSave = () => {
if (validateForm()) {
const notificationToSave: Partial<PmFaultNotification> = {
...notificationData,
id: `FN${Date.now()}`,
reportedAt: new Date(),
creationTime: new Date(),
lastModificationTime: new Date(),
}
onSave(notificationToSave)
onClose()
// Reset form
setNotificationData({
notificationCode: `ARZ-${new Date().getFullYear()}-${String(Date.now()).slice(-3)}`,
faultType: FaultTypeEnum.Mechanical,
priority: PriorityEnum.Normal,
severity: CriticalityLevelEnum.Medium,
status: NotificationStatusEnum.Open,
followUpRequired: false,
isActive: true,
images: [],
})
setUploadedImages([])
setErrors({})
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full max-w-3xl max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Yeni Arıza Bildirimi</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors">
<FaTimes className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-3 space-y-3">
{/* Basic Info */}
<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">
Bildirim Kodu *
</label>
<input
type="text"
value={notificationData.notificationCode || ''}
onChange={(e) => handleInputChange('notificationCode', e.target.value)}
className="w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="ARZ-2024-001"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">İş Merkezi *</label>
<select
value={notificationData.workCenterId || ''}
onChange={(e) => handleWorkCenterChange(e.target.value)}
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.workCenterId ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">İş merkezi seçin</option>
{mockWorkCenters.map((workCenter) => (
<option key={workCenter.id} value={workCenter.id}>
{workCenter.code} - {workCenter.name}
</option>
))}
</select>
{errors.workCenterId && (
<p className="text-red-500 text-xs mt-1">{errors.workCenterId}</p>
)}
</div>
</div>
{/* Title and Description */}
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Arıza Başlığı *
</label>
<input
type="text"
value={notificationData.title || ''}
onChange={(e) => handleInputChange('title', e.target.value)}
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.title ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Arıza başlığı"
/>
{errors.title && <p className="text-red-500 text-xs mt-1">{errors.title}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Detaylı ıklama *
</label>
<textarea
value={notificationData.description || ''}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={2}
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.description ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Arızanın detaylııklaması, belirtiler ve gözlemler"
/>
{errors.description && (
<p className="text-red-500 text-xs mt-1">{errors.description}</p>
)}
</div>
</div>
{/* Location and Reporter */}
<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">Konum *</label>
<input
type="text"
value={notificationData.location || ''}
onChange={(e) => handleInputChange('location', e.target.value)}
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.location ? 'border-red-500' : 'border-gray-300'
}`}
placeholder="Atölye A - Hat 1"
/>
{errors.location && <p className="text-red-500 text-xs mt-1">{errors.location}</p>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bildiren Kişi *
</label>
<select
value={notificationData.reportedBy || ''}
onChange={(e) => handleInputChange('reportedBy', e.target.value)}
className={`w-full px-2.5 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
errors.reportedBy ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Bildiren kişi seçin</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.fullName}>
{employee.fullName} - {employee.jobPosition?.name}
</option>
))}
</select>
{errors.reportedBy && (
<p className="text-red-500 text-xs mt-1">{errors.reportedBy}</p>
)}
</div>
</div>
<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">Arıza Türü</label>
<select
value={notificationData.faultType || FaultTypeEnum.Mechanical}
onChange={(e) => handleInputChange('faultType', e.target.value as FaultTypeEnum)}
className="w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{Object.values(FaultTypeEnum).map((faultType) => (
<option key={faultType} value={faultType}>
{getFaultTypeText(faultType)}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Öncelik</label>
<select
value={notificationData.priority || PriorityEnum.Normal}
onChange={(e) => handleInputChange('priority', e.target.value as PriorityEnum)}
className="w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{Object.values(PriorityEnum).map((priority) => (
<option key={priority} value={priority}>
{getPriorityText(priority)}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Kritiklik</label>
<select
value={notificationData.severity || CriticalityLevelEnum.Medium}
onChange={(e) =>
handleInputChange('severity', e.target.value as CriticalityLevelEnum)
}
className="w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
{Object.values(CriticalityLevelEnum).map((level) => (
<option key={level} value={level}>
{getCriticalityLevelText(level)}
</option>
))}
</select>
</div>
</div>
{/* Estimated Repair Time */}
<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">
Tahmini Onarım Süresi (dakika)
</label>
<input
type="number"
value={notificationData.estimatedRepairTime || ''}
onChange={(e) =>
handleInputChange('estimatedRepairTime', parseInt(e.target.value) || undefined)
}
className="w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="120"
min="0"
/>
</div>
<div className="flex items-center">
<label className="flex items-center">
<input
type="checkbox"
checked={notificationData.followUpRequired || false}
onChange={(e) => handleInputChange('followUpRequired', e.target.checked)}
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
/>
<span className="ml-2 text-sm text-gray-700">Takip gerekli</span>
</label>
</div>
</div>
{/* Image Upload */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Fotoğraflar</label>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4">
<div className="text-center">
<FaUpload className="mx-auto h-10 w-10 text-gray-400" />
<div className="mt-4">
<label htmlFor="image-upload" className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-gray-900">
Fotoğraf yükleyin
</span>
<span className="mt-1 block text-sm text-gray-500">
PNG, JPG, GIF formatında dosyalar
</span>
</label>
<input
id="image-upload"
name="image-upload"
type="file"
multiple
accept="image/*"
className="sr-only"
onChange={handleImageUpload}
/>
</div>
</div>
</div>
{uploadedImages.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium text-gray-700 mb-1">Yüklenen Fotoğraflar:</h4>
<div className="space-y-2">
{uploadedImages.map((image, index) => (
<div
key={index}
className="flex items-center justify-between p-1.5 bg-gray-50 rounded"
>
<span className="text-sm text-gray-600">{image}</span>
<button
type="button"
onClick={() => removeImage(index)}
className="text-red-600 hover:text-red-800"
>
<FaMinus className="w-3 h-3" />
</button>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-end space-x-2 p-3 border-t border-gray-200">
<button
onClick={onClose}
className="px-3 py-1 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
İptal
</button>
<button
onClick={handleSave}
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center space-x-2"
>
<FaSave className="w-4 h-4" />
<span>Kaydet</span>
</button>
</div>
</div>
</div>
)
}
export default NewFaultNotificationModal