erp-platform/ui/src/views/project/components/ProjectForm.tsx
2025-10-31 15:49:20 +03:00

1790 lines
79 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, useEffect, useCallback } from 'react'
import { useNavigate, useParams, Link } from 'react-router-dom'
import {
FaSave,
FaTimes,
FaArrowLeft,
FaFolder,
FaBullseye,
FaFlag,
FaTasks,
FaFileAlt,
FaExclamationCircle,
FaChartLine,
FaPlus,
FaEdit,
FaTrash,
FaBuilding,
FaPhone,
FaEnvelope,
FaClock,
FaUser,
FaDollarSign,
FaEye,
FaDownload,
} from 'react-icons/fa'
import LoadingSpinner from '../../../components/common/LoadingSpinner'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import {
PsProject,
ProjectStatusEnum,
ProjectTypeEnum,
PsProjectPhase,
PsProjectTask,
PsProjectRisk,
PsProjectDocument,
TaskStatusEnum,
PsDocumentTypeEnum,
RiskCategoryEnum,
RiskProbabilityEnum,
RiskImpactEnum,
RiskLevelEnum,
RiskStatusEnum,
} from '../../../types/ps'
import { mockProjects } from '../../../mocks/mockProjects'
import { mockProjectPhases } from '../../../mocks/mockProjectPhases'
import { mockProjectTasks } from '../../../mocks/mockProjectTasks'
import dayjs from 'dayjs'
import classNames from 'classnames'
import PhaseEditModal from './PhaseEditModal'
import TaskModal, { TaskFormData } from './TaskEditModal'
import DocumentUploadModal from './DocumentUploadModal'
import DocumentViewerModal from './DocumentViewerModal'
import RiskModal from './RiskModal'
import { BusinessParty, PriorityEnum } from '../../../types/common'
import {
getProjectStatusColor,
getProjectStatusIcon,
getProjectStatusText,
getProgressColor,
getPriorityColor,
getPriorityText,
getProjectTypeColor,
getProjectTypeText,
} from '../../../utils/erp'
import { Container } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { mockCurrencies } from '@/mocks/mockCurrencies'
import { EmployeeDto } from '@/proxy/intranet/models'
// Custom styles for the slider
const sliderStyles = `
input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 8px;
border-radius: 4px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #3b82f6;
border: 2px solid #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #3b82f6;
border: 2px solid #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
cursor: pointer;
}
`
interface ValidationErrors {
[key: string]: string
}
const ProjectForm: 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 [customers, setCustomers] = useState<BusinessParty[]>([])
const [projectManagers, setProjectManagers] = useState<EmployeeDto[]>([])
const [activeTab, setActiveTab] = useState('overview')
// Additional states for the new features
const [phases, setPhases] = useState<PsProjectPhase[]>([])
const [tasks, setTasks] = useState<PsProjectTask[]>([])
const [risks, setRisks] = useState<PsProjectRisk[]>([])
const [documents, setDocuments] = useState<PsProjectDocument[]>([])
// Modal states
const [isPhaseModalOpen, setIsPhaseModalOpen] = useState(false)
const [editingPhase, setEditingPhase] = useState<PsProjectPhase | null>(null)
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false)
const [editingTask, setEditingTask] = useState<PsProjectTask | null>(null)
// Document modal states
const [isDocumentUploadModalOpen, setIsDocumentUploadModalOpen] = useState(false)
const [isDocumentViewerModalOpen, setIsDocumentViewerModalOpen] = useState(false)
const [selectedDocument, setSelectedDocument] = useState<PsProjectDocument | null>(null)
// Risk modal states
const [isRiskModalOpen, setIsRiskModalOpen] = useState(false)
const [editingRisk, setEditingRisk] = useState<PsProjectRisk | null>(null)
const [formData, setFormData] = useState<PsProject>({
id: '',
code: '',
name: '',
description: '',
projectType: ProjectTypeEnum.Internal,
status: ProjectStatusEnum.Planning,
priority: PriorityEnum.Low,
customerId: '',
customer: undefined,
projectManagerId: '',
projectManager: undefined,
startDate: new Date(),
endDate: new Date(),
actualStartDate: undefined,
actualEndDate: undefined,
budget: 0,
actualCost: 0,
currency: 'TRY',
progress: 0,
phases: [],
tasks: [],
risks: [],
documents: [],
isActive: true,
creationTime: new Date(),
lastModificationTime: new Date(),
})
const tabs = [
{ id: 'overview', name: 'Genel Bakış', icon: FaChartLine },
{ id: 'phases', name: 'Aşamalar', icon: FaFlag },
{ id: 'tasks', name: 'Görevler', icon: FaTasks },
{ id: 'documents', name: 'Belgeler', icon: FaFileAlt },
{ id: 'risks', name: 'Riskler', icon: FaExclamationCircle },
{ id: 'budget', name: 'Bütçe Bilgileri', icon: FaDollarSign },
]
const budgetUsagePercentage =
formData.budget > 0 ? (formData.actualCost / formData.budget) * 100 : 0
const loadData = useCallback(async () => {
try {
setCustomers(mockBusinessParties)
setProjectManagers(mockEmployees)
if (isEdit && id) {
// Load existing phases, tasks, risks, documents for the project
const existingPhases = mockProjectPhases.filter((p) => p.projectId === id)
const existingTasks = mockProjectTasks.filter((t) => t.projectId === id)
setPhases(existingPhases)
setTasks(existingTasks)
// For now, using empty arrays for risks and documents
setRisks([])
setDocuments([])
}
} catch (error) {
console.error('Error loading data:', error)
}
}, [isEdit, id])
const loadFormData = useCallback(async () => {
setLoading(true)
try {
if (isEdit && id) {
await new Promise((resolve) => setTimeout(resolve, 1000))
const mockProject = mockProjects.find((p) => p.id === id)
if (mockProject) {
setFormData(mockProject)
}
}
} catch (error) {
console.error('Error loading form data:', error)
} finally {
setLoading(false)
}
}, [isEdit, id])
useEffect(() => {
loadData()
loadFormData()
}, [loadData, loadFormData])
// Add custom slider styles
useEffect(() => {
const style = document.createElement('style')
style.textContent = sliderStyles
document.head.appendChild(style)
return () => {
document.head.removeChild(style)
}
}, [])
const validateForm = (): boolean => {
const newErrors: ValidationErrors = {}
if (!formData.code.trim()) {
newErrors.projectCode = 'Proje kodu zorunludur'
}
if (!formData.name.trim()) {
newErrors.name = 'Proje adı zorunludur'
}
if (!formData.projectType) {
newErrors.projectType = 'Proje tipi seçilmelidir'
}
if (!formData.projectManagerId) {
newErrors.projectManagerId = 'Proje yöneticisi seçilmelidir'
}
if (!formData.startDate) {
newErrors.startDate = 'Başlangıç tarihi zorunludur'
}
if (!formData.endDate) {
newErrors.endDate = 'Bitiş tarihi zorunludur'
}
if (formData.startDate && formData.endDate && formData.startDate >= formData.endDate) {
newErrors.endDate = 'Bitiş tarihi başlangıç tarihinden sonra olmalıdır'
}
if (formData.budget <= 0) {
newErrors.budget = "Bütçe 0'dan büyük olmalıdır"
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleInputChange = (field: keyof PsProject, value: string | number | boolean | Date) => {
setFormData((prev) => ({
...prev,
[field]: value,
}))
if (errors[field]) {
setErrors((prev) => ({
...prev,
[field]: '',
}))
}
}
const handleDateChange = (field: keyof PsProject, value: string) => {
const date = value ? new Date(value) : new Date()
handleInputChange(field, date)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validateForm()) {
return
}
setSaving(true)
try {
await new Promise((resolve) => setTimeout(resolve, 2000))
console.log('Project data:', {
...formData,
id: isEdit ? id : undefined,
})
alert(isEdit ? 'Proje başarıyla güncellendi!' : 'Proje başarıyla oluşturuldu!')
navigate(ROUTES_ENUM.protected.projects.list)
} catch (error) {
console.error('Error saving project:', error)
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
} finally {
setSaving(false)
}
}
const handleCancel = () => {
navigate(ROUTES_ENUM.protected.projects.list)
}
// Phase modal handlers
const openPhaseModal = () => {
setEditingPhase(null)
setIsPhaseModalOpen(true)
}
const openEditPhaseModal = (phase: PsProjectPhase) => {
setEditingPhase(phase)
setIsPhaseModalOpen(true)
}
const closePhaseModal = () => {
setIsPhaseModalOpen(false)
setEditingPhase(null)
}
const handlePhaseSubmit = (phaseData: PsProjectPhase) => {
if (editingPhase) {
// Edit existing phase
const updatedPhases = phases.map((p) =>
p.id === editingPhase.id
? {
...editingPhase,
name: phaseData.name,
description: phaseData.description,
projectId: phaseData.projectId,
status: phaseData.status,
startDate: new Date(phaseData.startDate),
endDate: new Date(phaseData.endDate),
budget: phaseData.budget,
category: phaseData.category,
assignedTeams: phaseData.assignedTeams,
updatedAt: new Date(),
}
: p,
)
setPhases(updatedPhases)
alert('Aşama başarıyla güncellendi!')
} else {
// Create new phase
const selectedProject = mockProjects.find((p) => p.id === phaseData.projectId)
if (!selectedProject) {
alert('Proje bulunamadı!')
return
}
const newPhase: PsProjectPhase = {
id: Date.now().toString(),
code: `PH-${(phases.length + 1).toString().padStart(3, '0')}`,
name: phaseData.name,
description: phaseData.description,
projectId: phaseData.projectId,
project: selectedProject,
status: phaseData.status,
startDate: new Date(phaseData.startDate),
endDate: new Date(phaseData.endDate),
budget: phaseData.budget,
actualCost: 0,
progress: 0,
category: phaseData.category,
assignedTeams: phaseData.assignedTeams,
deliverables: [],
risks: [],
milestones: 0,
completedMilestones: 0,
sequence: phases.length + 1,
tasks: [],
isActive: true,
}
setPhases([...phases, newPhase])
alert('Yeni aşama başarıyla oluşturuldu!')
}
closePhaseModal()
}
const handleDeletePhase = (phaseId: string) => {
if (window.confirm('Bu aşamayı silmek istediğinizden emin misiniz?')) {
setPhases(phases.filter((p) => p.id !== phaseId))
alert('Aşama başarıyla silindi!')
}
}
// Task handlers
const openTaskModal = () => {
setEditingTask(null)
setIsTaskModalOpen(true)
}
const openEditTaskModal = (task: PsProjectTask) => {
setEditingTask(task)
setIsTaskModalOpen(true)
}
const closeTaskModal = () => {
setIsTaskModalOpen(false)
setEditingTask(null)
}
const handleTaskSubmit = (taskData: TaskFormData) => {
if (editingTask) {
// Edit existing task
const updatedTasks = tasks.map((t) =>
t.id === editingTask.id
? {
...editingTask,
name: taskData.name,
description: taskData.description,
projectId: taskData.projectId,
phaseId: taskData.phaseId,
taskType: taskData.taskType,
status: taskData.status,
priority: taskData.priority,
assignedTo: taskData.assignedTo,
assignee: mockEmployees.find((emp) => emp.id === taskData.assignedTo),
startDate: new Date(taskData.startDate),
endDate: new Date(taskData.endDate),
estimatedHours: taskData.estimatedHours,
progress: taskData.progress,
lastModificationTime: new Date(),
}
: t,
)
setTasks(updatedTasks)
alert('Görev başarıyla güncellendi!')
} else {
// Create new task
const selectedProject = mockProjects.find((p) => p.id === taskData.projectId)
const selectedPhase = mockProjectPhases.find((p) => p.id === taskData.phaseId)
const selectedAssignee = mockEmployees.find((emp) => emp.id === taskData.assignedTo)
if (!selectedProject) {
alert('Proje bulunamadı!')
return
}
const newTask: PsProjectTask = {
id: Date.now().toString(),
projectId: taskData.projectId,
phaseId: taskData.phaseId,
phase: selectedPhase,
taskCode: `TSK-${(tasks.length + 1).toString().padStart(3, '0')}`,
name: taskData.name,
description: taskData.description,
taskType: taskData.taskType,
status: taskData.status,
priority: taskData.priority,
assignedTo: taskData.assignedTo,
assignee: selectedAssignee,
startDate: new Date(taskData.startDate),
endDate: new Date(taskData.endDate),
estimatedHours: taskData.estimatedHours,
actualHours: 0,
progress: 0,
activities: [],
comments: [],
isActive: true,
creationTime: new Date(),
lastModificationTime: new Date(),
}
setTasks([...tasks, newTask])
alert('Yeni görev başarıyla oluşturuldu!')
}
closeTaskModal()
}
const handleDeleteTask = (taskId: string) => {
if (window.confirm('Bu görevi silmek istediğinizden emin misiniz?')) {
setTasks(tasks.filter((t) => t.id !== taskId))
alert('Görev başarıyla silindi!')
}
}
// Document handlers
const openDocumentUploadModal = () => {
setIsDocumentUploadModalOpen(true)
}
const closeDocumentUploadModal = () => {
setIsDocumentUploadModalOpen(false)
}
const handleDocumentUpload = (documentData: {
documentName: string
documentType: PsDocumentTypeEnum
file: File
}) => {
// In a real application, you would upload the file to a server
// For now, we'll create a mock URL and add it to the documents list
const newDocument: PsProjectDocument = {
id: Date.now().toString(),
projectId: formData.id || 'temp-project-id',
documentName: documentData.documentName,
documentType: documentData.documentType,
filePath: URL.createObjectURL(documentData.file), // Mock file path
fileSize: Number((documentData.file.size / (1024 * 1024)).toFixed(2)), // Convert to MB
uploadedBy: 'Current User', // In real app, get from auth context
uploadedAt: new Date(),
version: '1.0',
isActive: true,
}
setDocuments([...documents, newDocument])
closeDocumentUploadModal()
alert('Belge başarıyla yüklendi!')
}
const openDocumentViewer = (document: PsProjectDocument) => {
setSelectedDocument(document)
setIsDocumentViewerModalOpen(true)
}
const closeDocumentViewer = () => {
setIsDocumentViewerModalOpen(false)
setSelectedDocument(null)
}
const handleDocumentDownload = (document: PsProjectDocument) => {
// In a real application, this would trigger a server download
const link = window.document.createElement('a')
link.href = document.filePath
link.download = document.documentName
link.click()
}
const handleDeleteDocument = (documentId: string) => {
if (window.confirm('Bu belgeyi silmek istediğinizden emin misiniz?')) {
setDocuments(documents.filter((d) => d.id !== documentId))
alert('Belge başarıyla silindi!')
}
}
// Risk handlers
const openRiskModal = () => {
setEditingRisk(null)
setIsRiskModalOpen(true)
}
const openEditRiskModal = (risk: PsProjectRisk) => {
setEditingRisk(risk)
setIsRiskModalOpen(true)
}
const closeRiskModal = () => {
setIsRiskModalOpen(false)
setEditingRisk(null)
}
const handleRiskSubmit = (riskData: Partial<PsProjectRisk>) => {
if (editingRisk) {
// Edit existing risk
const updatedRisks = risks.map((r) =>
r.id === editingRisk.id
? {
...editingRisk,
...riskData,
}
: r,
)
setRisks(updatedRisks)
alert('Risk başarıyla güncellendi!')
} else {
// Create new risk
const newRisk: PsProjectRisk = {
id: Date.now().toString(),
projectId: formData.id || 'temp-project-id',
riskCode: `RISK-${(risks.length + 1).toString().padStart(3, '0')}`,
title: riskData.title || '',
description: riskData.description || '',
category: riskData.category || RiskCategoryEnum.Technical,
probability: riskData.probability || RiskProbabilityEnum.Medium,
impact: riskData.impact || RiskImpactEnum.Medium,
riskLevel: riskData.riskLevel || RiskLevelEnum.Medium,
status: riskData.status || RiskStatusEnum.Identified,
identifiedBy: 'Current User', // In real app, get from auth context
identifiedDate: new Date(),
mitigationPlan: riskData.mitigationPlan,
contingencyPlan: riskData.contingencyPlan,
ownerId: riskData.ownerId,
reviewDate: riskData.reviewDate,
isActive: true,
}
setRisks([...risks, newRisk])
alert('Yeni risk başarıyla eklendi!')
}
closeRiskModal()
}
const handleDeleteRisk = (riskId: string) => {
if (window.confirm('Bu riski silmek istediğinizden emin misiniz?')) {
setRisks(risks.filter((r) => r.id !== riskId))
alert('Risk başarıyla silindi!')
}
}
if (loading) {
return <LoadingSpinner />
}
return (
<Container>
<div className="space-y-2">
{/* Header */}
<div className="bg-white border-b border-gray-200">
<div className="p-2">
<div className="flex items-center justify-between h-12">
<div className="flex items-center">
<Link
to={ROUTES_ENUM.protected.projects.list}
className="flex items-center px-2.5 py-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors mr-2"
>
<FaArrowLeft className="w-4 h-4 mr-2" />
Geri
</Link>
<div className="flex items-center space-x-2">
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<FaFolder className="w-6 h-6 text-blue-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">
{isEdit ? formData.code : 'Yeni Proje'}
</h2>
<p className="text-gray-600">
{isEdit ? formData.name : 'Proje bilgilerini girin'}
</p>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{isEdit && (
<span
className={classNames(
'inline-flex items-center px-2.5 py-1 rounded-full text-sm font-medium border',
getProjectStatusColor(formData.status),
)}
>
{getProjectStatusIcon(formData.status)}
<span className="ml-2">{getProjectStatusText(formData.status)}</span>
</span>
)}
</div>
</div>
</div>
</div>
<div className="mx-auto py-2">
{/* Summary Cards - Only show in edit mode */}
{isEdit && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
{/* Progress Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between mb-2">
<div className="w-7 h-7 bg-blue-100 rounded-lg flex items-center justify-center">
<FaBullseye className="w-5 h-5 text-blue-600" />
</div>
<span className="text-lg font-bold text-blue-600">{formData.progress}%</span>
</div>
<h3 className="text-sm font-medium text-gray-600 mb-1">İlerleme</h3>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={classNames('h-2 rounded-full', getProgressColor(formData.progress))}
style={{ width: `${formData.progress}%` }}
/>
</div>
</div>
{/* Budget Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between mb-2">
<div className="w-7 h-7 bg-green-100 rounded-lg flex items-center justify-center">
<FaDollarSign className="w-5 h-5 text-green-600" />
</div>
<span className="text-lg font-bold text-green-600">
{budgetUsagePercentage.toFixed(0)}%
</span>
</div>
<h3 className="text-sm font-medium text-gray-600 mb-1">Bütçe Kullanımı</h3>
<div className="text-xs text-gray-500">
{formData.actualCost.toLocaleString()} / {formData.budget.toLocaleString()}
</div>
</div>
{/* Duration Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between mb-2">
<div className="w-7 h-7 bg-orange-100 rounded-lg flex items-center justify-center">
<FaClock className="w-5 h-5 text-orange-600" />
</div>
<span className="text-lg font-bold text-orange-600">
{dayjs(formData.endDate).diff(dayjs(formData.startDate), 'day')}
</span>
</div>
<h3 className="text-sm font-medium text-gray-600 mb-1">Süre (Gün)</h3>
<div className="text-xs text-gray-500">
{dayjs(formData.startDate).format('DD.MM.YYYY')} -{' '}
{dayjs(formData.endDate).format('DD.MM.YYYY')}
</div>
</div>
{/* Priority Card */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-3">
<div className="flex items-center justify-between mb-2">
<div className="w-7 h-7 bg-red-100 rounded-lg flex items-center justify-center">
<FaFlag className="w-5 h-5 text-red-600" />
</div>
<span
className={classNames('text-lg font-bold', getPriorityColor(formData.priority))}
>
{getPriorityText(formData.priority)}
</span>
</div>
<h3 className="text-sm font-medium text-gray-600 mb-1">Öncelik</h3>
<span
className={classNames(
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium border',
getProjectTypeColor(formData.projectType),
)}
>
{getProjectTypeText(formData.projectType)}
</span>
</div>
</div>
)}
{/* Tabs */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="border-b border-gray-200">
<nav className="flex space-x-2 px-3" aria-label="Tabs">
{tabs.map((tab) => {
const Icon = tab.icon
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={classNames(
'py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 transition-colors',
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
)}
>
<Icon className="w-4 h-4" />
<span>{tab.name}</span>
</button>
)
})}
</nav>
</div>
{/* Tab Content */}
<div className="p-3">
{activeTab === 'overview' && (
<div className="space-y-3">
{/* Project Info Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{/* Project Details */}
<div className="space-y-3">
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">
Proje Detayları
</h3>
<div className="bg-gray-50 rounded-lg p-2.5 space-y-2.5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Proje Kodu *
</label>
<input
autoFocus
type="text"
value={formData.code}
onChange={(e) => handleInputChange('code', e.target.value)}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.code
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
placeholder="Örn: PRJ001"
/>
{errors.projectCode && (
<p className="mt-1 text-sm text-red-600">{errors.projectCode}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Proje Tipi *
</label>
<select
value={formData.projectType}
onChange={(e) => handleInputChange('projectType', e.target.value)}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.projectType
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
>
<option value="">Tip seçin</option>
{Object.values(ProjectTypeEnum).map((type) => (
<option key={type} value={type}>
{getProjectTypeText(type)}
</option>
))}
</select>
{errors.projectType && (
<p className="mt-1 text-sm text-red-600">{errors.projectType}</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Proje Adı *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.name
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
placeholder="Proje adı"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2.5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Durum
</label>
<select
value={formData.status}
onChange={(e) => handleInputChange('status', e.target.value)}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{Object.values(ProjectStatusEnum).map((status) => (
<option key={status} value={status}>
{getProjectStatusText(status)}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Öncelik
</label>
<select
value={formData.priority}
onChange={(e) => handleInputChange('priority', e.target.value)}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{Object.values(PriorityEnum).map((priority) => (
<option key={priority} value={priority}>
{priority}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Para Birimi
</label>
<select
value={formData.currency}
onChange={(e) => handleInputChange('currency', e.target.value)}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
ıklama
</label>
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={3}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Proje açıklaması"
/>
</div>
</div>
</div>
{/* Timeline */}
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">
Zaman Çizelgesi
</h3>
<div className="bg-gray-50 rounded-lg p-2.5 space-y-2.5">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Başlangıç Tarihi *
</label>
<input
type="date"
value={dayjs(formData.startDate).format('YYYY-MM-DD')}
onChange={(e) => handleDateChange('startDate', e.target.value)}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.startDate
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
/>
{errors.startDate && (
<p className="mt-1 text-sm text-red-600">{errors.startDate}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Bitiş Tarihi *
</label>
<input
type="date"
value={dayjs(formData.endDate).format('YYYY-MM-DD')}
onChange={(e) => handleDateChange('endDate', e.target.value)}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.endDate
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
/>
{errors.endDate && (
<p className="mt-1 text-sm text-red-600">{errors.endDate}</p>
)}
</div>
</div>
{isEdit && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gerçek Başlangıç Tarihi
</label>
<input
type="date"
value={
formData.actualStartDate
? dayjs(formData.actualStartDate).format('YYYY-MM-DD')
: ''
}
onChange={(e) =>
handleDateChange('actualStartDate', e.target.value)
}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gerçek Bitiş Tarihi
</label>
<input
type="date"
value={
formData.actualEndDate
? dayjs(formData.actualEndDate).format('YYYY-MM-DD')
: ''
}
onChange={(e) =>
handleDateChange('actualEndDate', e.target.value)
}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
)}
</div>
</div>
{/* Progress Section */}
{isEdit && (
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">
İlerleme Durumu
</h3>
<div className="bg-gray-50 rounded-lg p-2.5">
<label className="block text-sm font-medium text-gray-700 mb-2">
Proje İlerleme: {formData.progress}%
</label>
<div className="relative">
<input
type="range"
min="0"
max="100"
value={formData.progress}
onChange={(e) =>
handleInputChange('progress', parseFloat(e.target.value) || 0)
}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${formData.progress}%, #e5e7eb ${formData.progress}%, #e5e7eb 100%)`,
}}
/>
<div className="flex justify-between text-xs text-gray-500 mt-1.5">
<span>0%</span>
<span>25%</span>
<span>50%</span>
<span>75%</span>
<span>100%</span>
</div>
</div>
<div className="mt-3 w-full bg-gray-200 rounded-full h-2.5">
<div
className={classNames(
'h-3 rounded-full transition-all duration-300',
getProgressColor(formData.progress),
)}
style={{ width: `${formData.progress}%` }}
/>
</div>
</div>
</div>
)}
{/* Active Status */}
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">
Durum Ayarları
</h3>
<div className="bg-gray-50 rounded-lg p-2.5">
<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">
Proje Aktif
</label>
</div>
</div>
</div>
</div>
{/* Stakeholders */}
<div className="space-y-3">
{/* Project Manager */}
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">
Proje Yöneticisi
</h3>
<div className="bg-gray-50 rounded-lg p-2.5">
<label className="block text-sm font-medium text-gray-700 mb-1">
Proje Yöneticisi *
</label>
<select
value={formData.projectManagerId}
onChange={(e) => handleInputChange('projectManagerId', e.target.value)}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.projectManagerId
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
>
<option value="">Proje yöneticisi seçin</option>
{projectManagers.map((manager) => (
<option key={manager.id} value={manager.id}>
{manager.fullName}
</option>
))}
</select>
{errors.projectManagerId && (
<p className="mt-1 text-sm text-red-600">{errors.projectManagerId}</p>
)}
{formData.projectManagerId && (
<div className="mt-1.5 p-2 bg-white rounded-lg border">
{(() => {
const manager = projectManagers.find(
(m) => m.id === formData.projectManagerId,
)
return manager ? (
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<FaUser className="w-5 h-5 text-blue-600" />
</div>
<div>
<h4 className="font-medium text-gray-900">
{manager.fullName}
</h4>
<p className="text-sm text-gray-600">
{manager.jobPosition?.name || 'Proje Yöneticisi'}
</p>
<div className="flex items-center mt-1 text-sm text-gray-500">
<FaEnvelope className="w-3 h-3 mr-1" />
{manager.email}
</div>
{manager.phone && (
<div className="flex items-center mt-1 text-sm text-gray-500">
<FaPhone className="w-3 h-3 mr-1" />
{manager.phone}
</div>
)}
</div>
</div>
) : null
})()}
</div>
)}
</div>
</div>
{/* Customer */}
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">Müşteri</h3>
<div className="bg-gray-50 rounded-lg p-2.5">
<label className="block text-sm font-medium text-gray-700 mb-1">
Müşteri
</label>
<select
value={formData.customerId}
onChange={(e) => handleInputChange('customerId', e.target.value)}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Müşteri seçin</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
{formData.customerId && (
<div className="mt-1.5 p-2 bg-white rounded-lg border">
{(() => {
const customer = customers.find((c) => c.id === formData.customerId)
return customer ? (
<div className="flex items-center space-x-3">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
<FaBuilding className="w-5 h-5 text-green-600" />
</div>
<div>
<h4 className="font-medium text-gray-900">{customer.name}</h4>
<p className="text-sm text-gray-600">
{customer.primaryContact?.firstName}{' '}
{customer.primaryContact?.lastName}
</p>
{customer.primaryContact?.email && (
<div className="flex items-center mt-1 text-sm text-gray-500">
<FaEnvelope className="w-3 h-3 mr-1" />
{customer.primaryContact.email}
</div>
)}
{customer.primaryContact?.phone && (
<div className="flex items-center mt-1 text-sm text-gray-500">
<FaPhone className="w-3 h-3 mr-1" />
{customer.primaryContact.phone}
</div>
)}
</div>
</div>
) : null
})()}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Budget Tab */}
{activeTab === 'budget' && (
<div className="space-y-3">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{/* Budget Information */}
<div className="space-y-3">
<div>
<h3 className="text-base font-semibold text-gray-900 mb-3">
Temel Bütçe Bilgileri
</h3>
<div className="bg-gray-50 rounded-lg p-3 space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Toplam Bütçe *
</label>
<div className="relative">
<input
type="number"
min="0"
step="0.01"
value={formData.budget}
onChange={(e) =>
handleInputChange('budget', parseFloat(e.target.value) || 0)
}
className={`block w-full px-2.5 py-1.5 text-sm border rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${
errors.budget
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
}`}
placeholder="0.00"
/>
<span className="absolute right-3 top-2 text-gray-500 text-sm">
{formData.currency}
</span>
</div>
{errors.budget && (
<p className="mt-1 text-sm text-red-600">{errors.budget}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Para Birimi
</label>
<select
value={formData.currency}
onChange={(e) => handleInputChange('currency', e.target.value)}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</option>
))}
</select>
</div>
{isEdit && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Gerçekleşen Maliyet
</label>
<div className="relative">
<input
type="number"
min="0"
step="0.01"
value={formData.actualCost}
onChange={(e) =>
handleInputChange('actualCost', parseFloat(e.target.value) || 0)
}
className="block w-full px-2.5 py-1.5 text-sm border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
/>
<span className="absolute right-3 top-2 text-gray-500 text-sm">
{formData.currency}
</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Budget Analysis */}
{isEdit && (
<div className="space-y-3">
<div>
<h3 className="text-base font-semibold text-gray-900 mb-3">
Bütçe Analizi
</h3>
<div className="bg-white rounded-lg border border-gray-200 p-3 space-y-3">
{/* Budget Usage Chart */}
<div>
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-medium text-gray-700">
Bütçe Kullanımı
</h4>
<span className="text-xl font-bold text-green-600">
{budgetUsagePercentage.toFixed(1)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 mb-1.5">
<div
className={classNames(
'h-3 rounded-full transition-all duration-300',
budgetUsagePercentage > 100
? 'bg-red-500'
: budgetUsagePercentage > 80
? 'bg-orange-500'
: budgetUsagePercentage > 60
? 'bg-yellow-500'
: 'bg-green-500',
)}
style={{
width: `${Math.min(budgetUsagePercentage, 100)}%`,
}}
/>
</div>
<div className="flex justify-between text-xs text-gray-500">
<span>{formData.actualCost.toLocaleString()}</span>
<span>{formData.budget.toLocaleString()}</span>
</div>
</div>
{/* Budget Statistics */}
<div className="grid grid-cols-1 gap-2">
<div className="bg-gray-50 rounded-lg p-2.5">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Kalan Bütçe</span>
<span
className={classNames(
'font-semibold',
formData.budget - formData.actualCost >= 0
? 'text-green-600'
: 'text-red-600',
)}
>
{(formData.budget - formData.actualCost).toLocaleString()}
</span>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-2.5">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">ılan Tutar</span>
<span
className={classNames(
'font-semibold',
formData.actualCost > formData.budget
? 'text-red-600'
: 'text-gray-400',
)}
>
{formData.actualCost > formData.budget
? `${(
formData.actualCost - formData.budget
).toLocaleString()}`
: '₺0'}
</span>
</div>
</div>
{budgetUsagePercentage > 80 && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-2.5">
<div className="flex items-center">
<FaExclamationCircle className="w-4 h-4 text-orange-600 mr-2" />
<span className="text-sm text-orange-800">
{budgetUsagePercentage > 100
? 'Bütçe aşıldı!'
: "Bütçe %80'in üzerinde kullanıldı!"}
</span>
</div>
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
</div>
)}
{/* Phases Tab */}
{activeTab === 'phases' && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Proje Fazları</h2>
<button
type="button"
onClick={openPhaseModal}
className="px-2.5 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<FaPlus className="w-4 h-4 mr-2" />
Faz Ekle
</button>
</div>
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="px-3 py-2 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900">
Faz Listesi ({phases.length})
</h3>
</div>
<div className="divide-y divide-gray-200">
{phases.length > 0 ? (
phases.map((phase, index) => (
<div key={phase.id || index} className="px-3 py-2 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1.5">
<h4 className="text-sm font-medium text-gray-900">
{phase.name}
</h4>
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{phase.code}
</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{phase.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-1.5">{phase.description}</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>
Başlangıç: {dayjs(phase.startDate).format('DD.MM.YYYY')}
</span>
<span>Bitiş: {dayjs(phase.endDate).format('DD.MM.YYYY')}</span>
<span>Bütçe: {phase.budget.toLocaleString()}</span>
<span>İlerleme: %{phase.progress}</span>
</div>
</div>
<div className="flex items-center space-x-1 ml-3">
<button
onClick={() => openEditPhaseModal(phase)}
className="p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeletePhase(phase.id)}
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
</div>
))
) : (
<div className="px-4 py-6 text-center">
<FaFlag className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">Faz bulunamadı</h3>
<p className="mt-1 text-sm text-gray-500">
Bu proje için henüz faz tanımlanmamış.
</p>
<button
onClick={openPhaseModal}
className="mt-2 px-2.5 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center mx-auto"
>
<FaPlus className="w-4 h-4 mr-2" />
İlk Fazı Ekle
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* Tasks Tab */}
{activeTab === 'tasks' && (
<div className="space-y-3">
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="flex items-center justify-between px-2.5 py-2 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">
Görev Listesi ({tasks.length})
</h3>
<button
type="button"
onClick={openTaskModal}
className="px-2.5 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<FaPlus className="w-4 h-4 mr-2" />
Görev Ekle
</button>
</div>
<div className="divide-y divide-gray-200">
{tasks.length > 0 ? (
tasks.map((task, index) => (
<div key={index} className="px-3 py-2 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1.5">
<h4 className="text-sm font-medium text-gray-900">{task.name}</h4>
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{task.taskCode}
</span>
<span
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${
task.status === TaskStatusEnum.Completed
? 'bg-green-100 text-green-800'
: task.status === TaskStatusEnum.InProgress
? 'bg-yellow-100 text-yellow-800'
: task.status === TaskStatusEnum.NotStarted
? 'bg-gray-100 text-gray-800'
: 'bg-red-100 text-red-800'
}`}
>
{task.status === TaskStatusEnum.Completed
? 'Tamamlandı'
: task.status === TaskStatusEnum.InProgress
? 'Devam Ediyor'
: task.status === TaskStatusEnum.NotStarted
? 'Başlamadı'
: task.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-1.5">{task.description}</p>
</div>
<div className="flex items-center space-x-1 ml-3">
<button
onClick={() => openEditTaskModal(task)}
className="p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteTask(task.id)}
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
</div>
))
) : (
<div className="px-3 py-6 text-center">
<FaTasks className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Görev bulunamadı
</h3>
<p className="mt-1 text-sm text-gray-500">
Bu proje için henüz görev tanımlanmamış.
</p>
</div>
)}
</div>
</div>
</div>
)}
{/* Documents Tab */}
{activeTab === 'documents' && (
<div className="space-y-3">
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="flex items-center justify-between px-2.5 py-2 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900">
Belge Listesi ({documents.length})
</h3>
<button
type="button"
onClick={openDocumentUploadModal}
className="px-2.5 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<FaPlus className="w-4 h-4 mr-2" />
Belge Yükle
</button>
</div>
<div className="divide-y divide-gray-200">
{documents.length > 0 ? (
documents.map((doc, index) => (
<div key={index} className="px-3 py-2 hover:bg-gray-50">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-7 h-7 bg-blue-100 rounded-lg flex items-center justify-center">
<FaFileAlt className="w-4 h-4 text-blue-600" />
</div>
<div>
<h4 className="text-sm font-medium text-gray-900">
{doc.documentName}
</h4>
<div className="flex items-center space-x-3 text-xs text-gray-500 mt-1">
<span>{doc.documentType}</span>
<span>{doc.fileSize} MB</span>
<span>{dayjs(doc.uploadedAt).format('DD.MM.YYYY')}</span>
<span>Yükleyen: {doc.uploadedBy}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-1">
<button
onClick={() => openDocumentViewer(doc)}
className="p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
title="Belgeyi Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleDocumentDownload(doc)}
className="p-1 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-lg"
title="Belgeyi İndir"
>
<FaDownload className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteDocument(doc.id)}
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg"
title="Belgeyi Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
</div>
))
) : (
<div className="px-3 py-6 text-center">
<FaFileAlt className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Belge bulunamadı
</h3>
<p className="mt-1 text-sm text-gray-500">
Bu proje için henüz belge yüklenmemiş.
</p>
</div>
)}
</div>
</div>
</div>
)}
{/* Risks Tab */}
{activeTab === 'risks' && (
<div className="space-y-3">
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="flex items-center justify-between px-2.5 py-2 border-b border-gray-200">
<h3 className="text-base font-medium text-gray-900">
Risk Listesi ({risks.length})
</h3>
<button
type="button"
onClick={openRiskModal}
className="px-2.5 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center"
>
<FaPlus className="w-4 h-4 mr-2" />
Risk Ekle
</button>
</div>
<div className="divide-y divide-gray-200">
{risks.length > 0 ? (
risks.map((risk, index) => (
<div key={index} className="px-3 py-2 hover:bg-gray-50">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-1.5">
<h4 className="text-sm font-medium text-gray-900">
{risk.title}
</h4>
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{risk.riskLevel}
</span>
<span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{risk.status}
</span>
</div>
<p className="text-sm text-gray-600 mb-1.5">{risk.description}</p>
{risk.mitigationPlan && (
<div className="bg-gray-50 rounded-lg p-2 mb-2">
<p className="text-xs font-medium text-gray-700 mb-1">
Önlem Planı:
</p>
<p className="text-xs text-gray-600">{risk.mitigationPlan}</p>
</div>
)}
</div>
<div className="flex items-center space-x-1 ml-3">
<button
onClick={() => openEditRiskModal(risk)}
className="p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
title="Risk Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteRisk(risk.id)}
className="p-1 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg"
title="Risk Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
</div>
))
) : (
<div className="px-3 py-6 text-center">
<FaExclamationCircle className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">
Risk bulunamadı
</h3>
<p className="mt-1 text-sm text-gray-500">
Bu proje için henüz risk tanımlanmamış.
</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
{/* Form Actions */}
<div className="flex items-center justify-end space-x-2 bg-white px-3 py-2 rounded-lg shadow-sm mt-3">
<button
type="button"
onClick={handleCancel}
className="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
<FaTimes className="w-4 h-4 mr-2" />
İptal
</button>
<button
type="submit"
disabled={saving}
onClick={handleSubmit}
className="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 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>
</div>
</div>
{/* Phase Modal */}
<PhaseEditModal
isOpen={isPhaseModalOpen}
onClose={closePhaseModal}
phase={editingPhase}
onSubmit={handlePhaseSubmit}
defaultProjectId={formData.id}
/>
{/* Task Modal */}
<TaskModal
isOpen={isTaskModalOpen}
onClose={closeTaskModal}
task={editingTask}
onSubmit={handleTaskSubmit}
mode={editingTask ? 'edit' : 'create'}
defaultProjectId={formData.id}
/>
{/* Document Upload Modal */}
<DocumentUploadModal
isOpen={isDocumentUploadModalOpen}
onClose={closeDocumentUploadModal}
onSubmit={handleDocumentUpload}
/>
{/* Document Viewer Modal */}
<DocumentViewerModal
isOpen={isDocumentViewerModalOpen}
onClose={closeDocumentViewer}
document={selectedDocument}
onDownload={handleDocumentDownload}
/>
{/* Risk Modal */}
<RiskModal
isOpen={isRiskModalOpen}
onClose={closeRiskModal}
risk={editingRisk}
onSubmit={handleRiskSubmit}
mode={editingRisk ? 'edit' : 'create'}
/>
</Container>
)
}
export default ProjectForm