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'
|
|
|
|
|
|
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-10-31 12:49:20 +00:00
|
|
|
|
import { EmployeeDto } from '@/proxy/intranet/models'
|
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
|
|
|
|
Açı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">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>
|
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
|