1790 lines
79 KiB
TypeScript
1790 lines
79 KiB
TypeScript
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">
|
||
Açı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">Aşı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
|