erp-platform/ui/src/views/project/components/ProjectForm.tsx

1791 lines
79 KiB
TypeScript
Raw Normal View History

2025-09-15 20:27:01 +00:00
import React, { useState, useEffect, useCallback } from 'react'
import { useNavigate, useParams, Link } from 'react-router-dom'
2025-09-15 09:31:47 +00:00
import {
FaSave,
FaTimes,
FaArrowLeft,
FaFolder,
FaBullseye,
FaFlag,
FaTasks,
FaFileAlt,
FaExclamationCircle,
FaChartLine,
FaPlus,
FaEdit,
FaTrash,
FaBuilding,
FaPhone,
FaEnvelope,
FaClock,
FaUser,
FaDollarSign,
FaEye,
FaDownload,
2025-09-15 20:27:01 +00:00
} from 'react-icons/fa'
import LoadingSpinner from '../../../components/common/LoadingSpinner'
2025-10-29 10:20:21 +00:00
import { EmployeeDto } from '../../../types/hr'
2025-09-15 20:27:01 +00:00
import { mockEmployees } from '../../../mocks/mockEmployees'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
2025-09-15 09:31:47 +00:00
import {
PsProject,
ProjectStatusEnum,
ProjectTypeEnum,
PsProjectPhase,
PsProjectTask,
PsProjectRisk,
PsProjectDocument,
TaskStatusEnum,
PsDocumentTypeEnum,
RiskCategoryEnum,
RiskProbabilityEnum,
RiskImpactEnum,
RiskLevelEnum,
RiskStatusEnum,
2025-09-15 20:27:01 +00:00
} 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'
2025-09-15 09:31:47 +00:00
import {
getProjectStatusColor,
getProjectStatusIcon,
getProjectStatusText,
getProgressColor,
getPriorityColor,
getPriorityText,
getProjectTypeColor,
getProjectTypeText,
2025-09-15 20:27:01 +00:00
} from '../../../utils/erp'
import { Container } from '@/components/shared'
2025-09-16 12:33:57 +00:00
import { ROUTES_ENUM } from '@/routes/route.constant'
2025-09-17 09:46:58 +00:00
import { mockCurrencies } from '@/mocks/mockCurrencies'
2025-09-15 09:31:47 +00:00
// 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;
}
2025-09-15 20:27:01 +00:00
`
2025-09-15 09:31:47 +00:00
interface ValidationErrors {
2025-09-15 20:27:01 +00:00
[key: string]: string
2025-09-15 09:31:47 +00:00
}
const ProjectForm: React.FC = () => {
2025-09-15 20:27:01 +00:00
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const isEdit = Boolean(id)
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState<ValidationErrors>({})
const [customers, setCustomers] = useState<BusinessParty[]>([])
2025-10-29 10:20:21 +00:00
const [projectManagers, setProjectManagers] = useState<EmployeeDto[]>([])
2025-09-15 20:27:01 +00:00
const [activeTab, setActiveTab] = useState('overview')
2025-09-15 09:31:47 +00:00
// Additional states for the new features
2025-09-15 20:27:01 +00:00
const [phases, setPhases] = useState<PsProjectPhase[]>([])
const [tasks, setTasks] = useState<PsProjectTask[]>([])
const [risks, setRisks] = useState<PsProjectRisk[]>([])
const [documents, setDocuments] = useState<PsProjectDocument[]>([])
2025-09-15 09:31:47 +00:00
// Modal states
2025-09-15 20:27:01 +00:00
const [isPhaseModalOpen, setIsPhaseModalOpen] = useState(false)
const [editingPhase, setEditingPhase] = useState<PsProjectPhase | null>(null)
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false)
const [editingTask, setEditingTask] = useState<PsProjectTask | null>(null)
2025-09-15 09:31:47 +00:00
// Document modal states
2025-09-15 20:27:01 +00:00
const [isDocumentUploadModalOpen, setIsDocumentUploadModalOpen] = useState(false)
const [isDocumentViewerModalOpen, setIsDocumentViewerModalOpen] = useState(false)
const [selectedDocument, setSelectedDocument] = useState<PsProjectDocument | null>(null)
2025-09-15 09:31:47 +00:00
// Risk modal states
2025-09-15 20:27:01 +00:00
const [isRiskModalOpen, setIsRiskModalOpen] = useState(false)
const [editingRisk, setEditingRisk] = useState<PsProjectRisk | null>(null)
2025-09-15 09:31:47 +00:00
const [formData, setFormData] = useState<PsProject>({
2025-09-15 20:27:01 +00:00
id: '',
code: '',
name: '',
description: '',
2025-09-15 09:31:47 +00:00
projectType: ProjectTypeEnum.Internal,
status: ProjectStatusEnum.Planning,
priority: PriorityEnum.Low,
2025-09-15 20:27:01 +00:00
customerId: '',
2025-09-15 09:31:47 +00:00
customer: undefined,
2025-09-15 20:27:01 +00:00
projectManagerId: '',
2025-09-15 09:31:47 +00:00
projectManager: undefined,
startDate: new Date(),
endDate: new Date(),
actualStartDate: undefined,
actualEndDate: undefined,
budget: 0,
actualCost: 0,
2025-09-15 20:27:01 +00:00
currency: 'TRY',
2025-09-15 09:31:47 +00:00
progress: 0,
phases: [],
tasks: [],
risks: [],
documents: [],
isActive: true,
creationTime: new Date(),
lastModificationTime: new Date(),
2025-09-15 20:27:01 +00:00
})
2025-09-15 09:31:47 +00:00
const tabs = [
2025-09-15 20:27:01 +00:00
{ 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 },
]
2025-09-15 09:31:47 +00:00
const budgetUsagePercentage =
2025-09-15 20:27:01 +00:00
formData.budget > 0 ? (formData.actualCost / formData.budget) * 100 : 0
2025-09-15 09:31:47 +00:00
const loadData = useCallback(async () => {
try {
2025-09-15 20:27:01 +00:00
setCustomers(mockBusinessParties)
setProjectManagers(mockEmployees)
2025-09-15 09:31:47 +00:00
if (isEdit && id) {
// Load existing phases, tasks, risks, documents for the project
2025-09-15 20:27:01 +00:00
const existingPhases = mockProjectPhases.filter((p) => p.projectId === id)
const existingTasks = mockProjectTasks.filter((t) => t.projectId === id)
setPhases(existingPhases)
setTasks(existingTasks)
2025-09-15 09:31:47 +00:00
// For now, using empty arrays for risks and documents
2025-09-15 20:27:01 +00:00
setRisks([])
setDocuments([])
2025-09-15 09:31:47 +00:00
}
} catch (error) {
2025-09-15 20:27:01 +00:00
console.error('Error loading data:', error)
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}, [isEdit, id])
2025-09-15 09:31:47 +00:00
const loadFormData = useCallback(async () => {
2025-09-15 20:27:01 +00:00
setLoading(true)
2025-09-15 09:31:47 +00:00
try {
if (isEdit && id) {
2025-09-15 20:27:01 +00:00
await new Promise((resolve) => setTimeout(resolve, 1000))
const mockProject = mockProjects.find((p) => p.id === id)
2025-09-15 09:31:47 +00:00
if (mockProject) {
2025-09-15 20:27:01 +00:00
setFormData(mockProject)
2025-09-15 09:31:47 +00:00
}
}
} catch (error) {
2025-09-15 20:27:01 +00:00
console.error('Error loading form data:', error)
2025-09-15 09:31:47 +00:00
} finally {
2025-09-15 20:27:01 +00:00
setLoading(false)
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}, [isEdit, id])
2025-09-15 09:31:47 +00:00
useEffect(() => {
2025-09-15 20:27:01 +00:00
loadData()
loadFormData()
}, [loadData, loadFormData])
2025-09-15 09:31:47 +00:00
// Add custom slider styles
useEffect(() => {
2025-09-15 20:27:01 +00:00
const style = document.createElement('style')
style.textContent = sliderStyles
document.head.appendChild(style)
2025-09-15 09:31:47 +00:00
return () => {
2025-09-15 20:27:01 +00:00
document.head.removeChild(style)
}
}, [])
2025-09-15 09:31:47 +00:00
const validateForm = (): boolean => {
2025-09-15 20:27:01 +00:00
const newErrors: ValidationErrors = {}
2025-09-15 09:31:47 +00:00
if (!formData.code.trim()) {
2025-09-15 20:27:01 +00:00
newErrors.projectCode = 'Proje kodu zorunludur'
2025-09-15 09:31:47 +00:00
}
if (!formData.name.trim()) {
2025-09-15 20:27:01 +00:00
newErrors.name = 'Proje adı zorunludur'
2025-09-15 09:31:47 +00:00
}
if (!formData.projectType) {
2025-09-15 20:27:01 +00:00
newErrors.projectType = 'Proje tipi seçilmelidir'
2025-09-15 09:31:47 +00:00
}
if (!formData.projectManagerId) {
2025-09-15 20:27:01 +00:00
newErrors.projectManagerId = 'Proje yöneticisi seçilmelidir'
2025-09-15 09:31:47 +00:00
}
if (!formData.startDate) {
2025-09-15 20:27:01 +00:00
newErrors.startDate = 'Başlangıç tarihi zorunludur'
2025-09-15 09:31:47 +00:00
}
if (!formData.endDate) {
2025-09-15 20:27:01 +00:00
newErrors.endDate = 'Bitiş tarihi zorunludur'
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
if (formData.startDate && formData.endDate && formData.startDate >= formData.endDate) {
newErrors.endDate = 'Bitiş tarihi başlangıç tarihinden sonra olmalıdır'
2025-09-15 09:31:47 +00:00
}
if (formData.budget <= 0) {
2025-09-15 20:27:01 +00:00
newErrors.budget = "Bütçe 0'dan büyük olmalıdır"
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
const handleInputChange = (field: keyof PsProject, value: string | number | boolean | Date) => {
2025-09-15 09:31:47 +00:00
setFormData((prev) => ({
...prev,
[field]: value,
2025-09-15 20:27:01 +00:00
}))
2025-09-15 09:31:47 +00:00
if (errors[field]) {
setErrors((prev) => ({
...prev,
2025-09-15 20:27:01 +00:00
[field]: '',
}))
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
const handleDateChange = (field: keyof PsProject, value: string) => {
2025-09-15 20:27:01 +00:00
const date = value ? new Date(value) : new Date()
handleInputChange(field, date)
}
2025-09-15 09:31:47 +00:00
const handleSubmit = async (e: React.FormEvent) => {
2025-09-15 20:27:01 +00:00
e.preventDefault()
2025-09-15 09:31:47 +00:00
if (!validateForm()) {
2025-09-15 20:27:01 +00:00
return
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
setSaving(true)
2025-09-15 09:31:47 +00:00
try {
2025-09-15 20:27:01 +00:00
await new Promise((resolve) => setTimeout(resolve, 2000))
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
console.log('Project data:', {
2025-09-15 09:31:47 +00:00
...formData,
id: isEdit ? id : undefined,
2025-09-15 20:27:01 +00:00
})
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
alert(isEdit ? 'Proje başarıyla güncellendi!' : 'Proje başarıyla oluşturuldu!')
2025-09-15 09:31:47 +00:00
2025-09-16 12:33:57 +00:00
navigate(ROUTES_ENUM.protected.projects.list)
2025-09-15 09:31:47 +00:00
} catch (error) {
2025-09-15 20:27:01 +00:00
console.error('Error saving project:', error)
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
2025-09-15 09:31:47 +00:00
} finally {
2025-09-15 20:27:01 +00:00
setSaving(false)
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
const handleCancel = () => {
2025-09-16 12:33:57 +00:00
navigate(ROUTES_ENUM.protected.projects.list)
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
// Phase modal handlers
const openPhaseModal = () => {
2025-09-15 20:27:01 +00:00
setEditingPhase(null)
setIsPhaseModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const openEditPhaseModal = (phase: PsProjectPhase) => {
2025-09-15 20:27:01 +00:00
setEditingPhase(phase)
setIsPhaseModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const closePhaseModal = () => {
2025-09-15 20:27:01 +00:00
setIsPhaseModalOpen(false)
setEditingPhase(null)
}
2025-09-15 09:31:47 +00:00
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(),
}
2025-09-15 20:27:01 +00:00
: p,
)
setPhases(updatedPhases)
alert('Aşama başarıyla güncellendi!')
2025-09-15 09:31:47 +00:00
} else {
// Create new phase
2025-09-15 20:27:01 +00:00
const selectedProject = mockProjects.find((p) => p.id === phaseData.projectId)
2025-09-15 09:31:47 +00:00
if (!selectedProject) {
2025-09-15 20:27:01 +00:00
alert('Proje bulunamadı!')
return
2025-09-15 09:31:47 +00:00
}
const newPhase: PsProjectPhase = {
id: Date.now().toString(),
2025-09-15 20:27:01 +00:00
code: `PH-${(phases.length + 1).toString().padStart(3, '0')}`,
2025-09-15 09:31:47 +00:00
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,
2025-09-15 20:27:01 +00:00
}
setPhases([...phases, newPhase])
alert('Yeni aşama başarıyla oluşturuldu!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
closePhaseModal()
}
2025-09-15 09:31:47 +00:00
const handleDeletePhase = (phaseId: string) => {
2025-09-15 20:27:01 +00:00
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!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
// Task handlers
const openTaskModal = () => {
2025-09-15 20:27:01 +00:00
setEditingTask(null)
setIsTaskModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const openEditTaskModal = (task: PsProjectTask) => {
2025-09-15 20:27:01 +00:00
setEditingTask(task)
setIsTaskModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const closeTaskModal = () => {
2025-09-15 20:27:01 +00:00
setIsTaskModalOpen(false)
setEditingTask(null)
}
2025-09-15 09:31:47 +00:00
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,
2025-09-15 20:27:01 +00:00
assignee: mockEmployees.find((emp) => emp.id === taskData.assignedTo),
2025-09-15 09:31:47 +00:00
startDate: new Date(taskData.startDate),
endDate: new Date(taskData.endDate),
estimatedHours: taskData.estimatedHours,
progress: taskData.progress,
lastModificationTime: new Date(),
}
2025-09-15 20:27:01 +00:00
: t,
)
setTasks(updatedTasks)
alert('Görev başarıyla güncellendi!')
2025-09-15 09:31:47 +00:00
} else {
// Create new task
2025-09-15 20:27:01 +00:00
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)
2025-09-15 09:31:47 +00:00
if (!selectedProject) {
2025-09-15 20:27:01 +00:00
alert('Proje bulunamadı!')
return
2025-09-15 09:31:47 +00:00
}
const newTask: PsProjectTask = {
id: Date.now().toString(),
projectId: taskData.projectId,
phaseId: taskData.phaseId,
phase: selectedPhase,
2025-09-15 20:27:01 +00:00
taskCode: `TSK-${(tasks.length + 1).toString().padStart(3, '0')}`,
2025-09-15 09:31:47 +00:00
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(),
2025-09-15 20:27:01 +00:00
}
setTasks([...tasks, newTask])
alert('Yeni görev başarıyla oluşturuldu!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
closeTaskModal()
}
2025-09-15 09:31:47 +00:00
const handleDeleteTask = (taskId: string) => {
2025-09-15 20:27:01 +00:00
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!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
// Document handlers
const openDocumentUploadModal = () => {
2025-09-15 20:27:01 +00:00
setIsDocumentUploadModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const closeDocumentUploadModal = () => {
2025-09-15 20:27:01 +00:00
setIsDocumentUploadModalOpen(false)
}
2025-09-15 09:31:47 +00:00
const handleDocumentUpload = (documentData: {
2025-09-15 20:27:01 +00:00
documentName: string
documentType: PsDocumentTypeEnum
file: File
2025-09-15 09:31:47 +00:00
}) => {
// 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(),
2025-09-15 20:27:01 +00:00
projectId: formData.id || 'temp-project-id',
2025-09-15 09:31:47 +00:00
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
2025-09-15 20:27:01 +00:00
uploadedBy: 'Current User', // In real app, get from auth context
2025-09-15 09:31:47 +00:00
uploadedAt: new Date(),
2025-09-15 20:27:01 +00:00
version: '1.0',
2025-09-15 09:31:47 +00:00
isActive: true,
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
setDocuments([...documents, newDocument])
closeDocumentUploadModal()
alert('Belge başarıyla yüklendi!')
}
2025-09-15 09:31:47 +00:00
const openDocumentViewer = (document: PsProjectDocument) => {
2025-09-15 20:27:01 +00:00
setSelectedDocument(document)
setIsDocumentViewerModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const closeDocumentViewer = () => {
2025-09-15 20:27:01 +00:00
setIsDocumentViewerModalOpen(false)
setSelectedDocument(null)
}
2025-09-15 09:31:47 +00:00
const handleDocumentDownload = (document: PsProjectDocument) => {
// In a real application, this would trigger a server download
2025-09-15 20:27:01 +00:00
const link = window.document.createElement('a')
link.href = document.filePath
link.download = document.documentName
link.click()
}
2025-09-15 09:31:47 +00:00
const handleDeleteDocument = (documentId: string) => {
2025-09-15 20:27:01 +00:00
if (window.confirm('Bu belgeyi silmek istediğinizden emin misiniz?')) {
setDocuments(documents.filter((d) => d.id !== documentId))
alert('Belge başarıyla silindi!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
// Risk handlers
const openRiskModal = () => {
2025-09-15 20:27:01 +00:00
setEditingRisk(null)
setIsRiskModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const openEditRiskModal = (risk: PsProjectRisk) => {
2025-09-15 20:27:01 +00:00
setEditingRisk(risk)
setIsRiskModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const closeRiskModal = () => {
2025-09-15 20:27:01 +00:00
setIsRiskModalOpen(false)
setEditingRisk(null)
}
2025-09-15 09:31:47 +00:00
const handleRiskSubmit = (riskData: Partial<PsProjectRisk>) => {
if (editingRisk) {
// Edit existing risk
const updatedRisks = risks.map((r) =>
r.id === editingRisk.id
? {
...editingRisk,
...riskData,
}
2025-09-15 20:27:01 +00:00
: r,
)
setRisks(updatedRisks)
alert('Risk başarıyla güncellendi!')
2025-09-15 09:31:47 +00:00
} else {
// Create new risk
const newRisk: PsProjectRisk = {
id: Date.now().toString(),
2025-09-15 20:27:01 +00:00
projectId: formData.id || 'temp-project-id',
riskCode: `RISK-${(risks.length + 1).toString().padStart(3, '0')}`,
title: riskData.title || '',
description: riskData.description || '',
2025-09-15 09:31:47 +00:00
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,
2025-09-15 20:27:01 +00:00
identifiedBy: 'Current User', // In real app, get from auth context
2025-09-15 09:31:47 +00:00
identifiedDate: new Date(),
mitigationPlan: riskData.mitigationPlan,
contingencyPlan: riskData.contingencyPlan,
ownerId: riskData.ownerId,
reviewDate: riskData.reviewDate,
isActive: true,
2025-09-15 20:27:01 +00:00
}
setRisks([...risks, newRisk])
alert('Yeni risk başarıyla eklendi!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
closeRiskModal()
}
2025-09-15 09:31:47 +00:00
const handleDeleteRisk = (riskId: string) => {
2025-09-15 20:27:01 +00:00
if (window.confirm('Bu riski silmek istediğinizden emin misiniz?')) {
setRisks(risks.filter((r) => r.id !== riskId))
alert('Risk başarıyla silindi!')
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
}
2025-09-15 09:31:47 +00:00
if (loading) {
2025-09-15 20:27:01 +00:00
return <LoadingSpinner />
2025-09-15 09:31:47 +00:00
}
return (
2025-09-15 20:27:01 +00:00
<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
2025-09-16 12:33:57 +00:00
to={ROUTES_ENUM.protected.projects.list}
2025-09-15 20:27:01 +00:00
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>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">
2025-09-15 20:27:01 +00:00
{isEdit ? formData.code : 'Yeni Proje'}
2025-09-15 21:02:48 +00:00
</h2>
<p className="text-gray-600">
2025-09-15 20:27:01 +00:00
{isEdit ? formData.name : 'Proje bilgilerini girin'}
</p>
</div>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
<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>
2025-09-15 09:31:47 +00:00
</span>
2025-09-15 20:27:01 +00:00
)}
</div>
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
2025-09-15 20:27:01 +00:00
<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}%` }}
/>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
{/* 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()}
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
{/* 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')}
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
{/* 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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
<h3 className="text-sm font-medium text-gray-600 mb-1">Öncelik</h3>
2025-09-15 09:31:47 +00:00
<span
className={classNames(
2025-09-15 20:27:01 +00:00
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium border',
getProjectTypeColor(formData.projectType),
2025-09-15 09:31:47 +00:00
)}
>
2025-09-15 20:27:01 +00:00
{getProjectTypeText(formData.projectType)}
2025-09-15 09:31:47 +00:00
</span>
</div>
</div>
2025-09-15 20:27:01 +00:00
)}
{/* 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>
2025-09-17 09:46:58 +00:00
{Object.values(ProjectTypeEnum).map((type) => (
<option key={type} value={type}>
{getProjectTypeText(type)}
</option>
))}
2025-09-15 20:27:01 +00:00
</select>
{errors.projectType && (
<p className="mt-1 text-sm text-red-600">{errors.projectType}</p>
)}
</div>
</div>
2025-09-15 09:31:47 +00:00
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
2025-09-15 20:27:01 +00:00
Proje Adı *
2025-09-15 09:31:47 +00:00
</label>
<input
type="text"
2025-09-15 20:27:01 +00:00
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
2025-09-15 09:31:47 +00:00
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 ${
2025-09-15 20:27:01 +00:00
errors.name
? 'border-red-300 focus:border-red-500'
: 'border-gray-300 focus:border-blue-500'
2025-09-15 09:31:47 +00:00
}`}
2025-09-15 20:27:01 +00:00
placeholder="Proje adı"
2025-09-15 09:31:47 +00:00
/>
2025-09-15 20:27:01 +00:00
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
2025-09-15 09:31:47 +00:00
)}
</div>
2025-09-15 20:27:01 +00:00
<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"
>
2025-09-17 09:46:58 +00:00
{Object.values(ProjectStatusEnum).map((status) => (
<option key={status} value={status}>
{getProjectStatusText(status)}
</option>
))}
2025-09-15 20:27:01 +00:00
</select>
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
<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"
>
2025-09-17 09:46:58 +00:00
{Object.values(PriorityEnum).map((priority) => (
<option key={priority} value={priority}>
{priority}
</option>
))}
2025-09-15 20:27:01 +00:00
</select>
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
<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"
>
2025-09-17 09:46:58 +00:00
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</option>
))}
2025-09-15 20:27:01 +00:00
</select>
</div>
2025-09-15 09:31:47 +00:00
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
2025-09-15 20:27:01 +00:00
ıklama
2025-09-15 09:31:47 +00:00
</label>
2025-09-15 20:27:01 +00:00
<textarea
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={3}
2025-09-15 09:31:47 +00:00
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"
2025-09-15 20:27:01 +00:00
placeholder="Proje açıklaması"
2025-09-15 09:31:47 +00:00
/>
</div>
</div>
2025-09-15 20:27:01 +00:00
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{/* 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">
2025-09-15 09:31:47 +00:00
<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">
2025-09-15 20:27:01 +00:00
Başlangıç Tarihi *
2025-09-15 09:31:47 +00:00
</label>
<input
type="date"
2025-09-15 20:27:01 +00:00
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'
}`}
2025-09-15 09:31:47 +00:00
/>
2025-09-15 20:27:01 +00:00
{errors.startDate && (
<p className="mt-1 text-sm text-red-600">{errors.startDate}</p>
)}
2025-09-15 09:31:47 +00:00
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
2025-09-15 20:27:01 +00:00
Bitiş Tarihi *
2025-09-15 09:31:47 +00:00
</label>
<input
type="date"
2025-09-15 20:27:01 +00:00
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}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
2025-09-15 20:27:01 +00:00
handleInputChange('progress', parseFloat(e.target.value) || 0)
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
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}%` }}
2025-09-15 09:31:47 +00:00
/>
</div>
</div>
2025-09-15 20:27:01 +00:00
</div>
)}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{/* Active Status */}
2025-09-15 09:31:47 +00:00
<div>
<h3 className="text-base font-semibold text-gray-900 mb-2">
2025-09-15 20:27:01 +00:00
Durum Ayarları
2025-09-15 09:31:47 +00:00
</h3>
<div className="bg-gray-50 rounded-lg p-2.5">
2025-09-15 20:27:01 +00:00
<div className="flex items-center">
2025-09-15 09:31:47 +00:00
<input
2025-09-15 20:27:01 +00:00
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"
2025-09-15 09:31:47 +00:00
/>
2025-09-15 20:27:01 +00:00
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
Proje Aktif
</label>
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
</div>
2025-09-15 20:27:01 +00:00
{/* 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>
)}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{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>
2025-09-15 09:31:47 +00:00
<div className="flex items-center mt-1 text-sm text-gray-500">
<FaEnvelope className="w-3 h-3 mr-1" />
2025-09-15 20:27:01 +00:00
{manager.email}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
{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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
) : null
})()}
</div>
2025-09-15 09:31:47 +00:00
)}
</div>
2025-09-15 20:27:01 +00:00
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{/* 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">
2025-09-15 09:31:47 +00:00
<label className="block text-sm font-medium text-gray-700 mb-1">
2025-09-15 20:27:01 +00:00
Müşteri
2025-09-15 09:31:47 +00:00
</label>
<select
2025-09-15 20:27:01 +00:00
value={formData.customerId}
onChange={(e) => handleInputChange('customerId', e.target.value)}
2025-09-15 09:31:47 +00:00
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"
>
2025-09-15 20:27:01 +00:00
<option value="">Müşteri seçin</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
2025-09-15 09:31:47 +00:00
</select>
2025-09-15 20:27:01 +00:00
{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>
)}
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
</div>
</div>
</div>
</div>
)}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{/* 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">
2025-09-15 09:31:47 +00:00
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
2025-09-15 20:27:01 +00:00
Toplam Bütçe *
2025-09-15 09:31:47 +00:00
</label>
<div className="relative">
<input
type="number"
min="0"
step="0.01"
2025-09-15 20:27:01 +00:00
value={formData.budget}
2025-09-15 09:31:47 +00:00
onChange={(e) =>
2025-09-15 20:27:01 +00:00
handleInputChange('budget', parseFloat(e.target.value) || 0)
2025-09-15 09:31:47 +00:00
}
2025-09-15 20:27:01 +00:00
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'
}`}
2025-09-15 09:31:47 +00:00
placeholder="0.00"
/>
<span className="absolute right-3 top-2 text-gray-500 text-sm">
{formData.currency}
</span>
</div>
2025-09-15 20:27:01 +00:00
{errors.budget && (
<p className="mt-1 text-sm text-red-600">{errors.budget}</p>
)}
2025-09-15 09:31:47 +00:00
</div>
<div>
2025-09-15 20:27:01 +00:00
<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"
>
2025-09-17 09:46:58 +00:00
{mockCurrencies.map((currency) => (
<option key={currency.value} value={currency.value}>
{currency.value} - {currency.label}
</option>
))}
2025-09-15 20:27:01 +00:00
</select>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
{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}
2025-09-15 09:31:47 +00:00
</span>
</div>
</div>
2025-09-15 20:27:01 +00:00
)}
</div>
</div>
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{/* 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)}%
2025-09-15 09:31:47 +00:00
</span>
2025-09-15 20:27:01 +00:00
</div>
<div className="w-full bg-gray-200 rounded-full h-3 mb-1.5">
<div
2025-09-15 09:31:47 +00:00
className={classNames(
2025-09-15 20:27:01 +00:00
'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',
2025-09-15 09:31:47 +00:00
)}
2025-09-15 20:27:01 +00:00
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>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
{/* 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()}
2025-09-15 09:31:47 +00:00
</span>
</div>
</div>
2025-09-15 20:27:01 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
{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>
)}
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
</div>
)}
</div>
</div>
2025-09-15 20:27:01 +00:00
)}
{/* 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>
2025-09-15 09:31:47 +00:00
<button
type="button"
2025-09-15 20:27:01 +00:00
onClick={openPhaseModal}
2025-09-15 09:31:47 +00:00
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" />
2025-09-15 20:27:01 +00:00
Faz Ekle
2025-09-15 09:31:47 +00:00
</button>
</div>
2025-09-15 20:27:01 +00:00
<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"
2025-09-15 09:31:47 +00:00
>
2025-09-15 20:27:01 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
2025-09-15 20:27:01 +00:00
))
) : (
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
)}
</div>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
)}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
{/* 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}
2025-09-15 09:31:47 +00:00
</span>
</div>
2025-09-15 20:27:01 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
2025-09-15 20:27:01 +00:00
))
) : (
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
)}
</div>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
)}
{/* 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" />
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
</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>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
))
) : (
<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>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
)}
</div>
2025-09-15 09:31:47 +00:00
</div>
</div>
2025-09-15 20:27:01 +00:00
)}
</div>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 20:27:01 +00:00
{/* 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>
2025-09-15 09:31:47 +00:00
</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}
2025-09-15 20:27:01 +00:00
mode={editingTask ? 'edit' : 'create'}
2025-09-15 09:31:47 +00:00
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}
2025-09-15 20:27:01 +00:00
mode={editingRisk ? 'edit' : 'create'}
2025-09-15 09:31:47 +00:00
/>
2025-09-15 20:27:01 +00:00
</Container>
)
}
2025-09-15 09:31:47 +00:00
2025-09-15 20:27:01 +00:00
export default ProjectForm