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

624 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import {
FaFolder,
FaPlus,
FaSearch,
FaFilter,
FaDownload,
FaEdit,
FaEye,
FaCalendar,
FaUser,
FaDollarSign,
FaExclamationTriangle,
FaBullseye,
FaTh,
FaList,
FaFlag,
FaTasks,
} from 'react-icons/fa'
import classNames from 'classnames'
import { ProjectStatusEnum, PsProject } from '../../../types/ps'
import dayjs from 'dayjs'
import { mockProjects } from '../../../mocks/mockProjects'
import PhaseViewModal from './PhaseViewModal'
import TaskViewModal from './TaskViewModal'
import Widget from '../../../components/common/Widget'
import { PriorityEnum } from '../../../types/common'
import {
getProjectStatusColor,
getProjectStatusIcon,
getProjectStatusText,
getPriorityColor,
getPriorityText,
getProgressColor,
} from '../../../utils/erp'
import { Container } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant'
const ProjectList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState('all')
const [filterPriority, setFilterPriority] = useState('all')
const [showFilters, setShowFilters] = useState(false)
const [viewMode, setViewMode] = useState<'card' | 'list'>('list')
// Modal states
const [phasesModalOpen, setPhasesModalOpen] = useState(false)
const [tasksModalOpen, setTasksModalOpen] = useState(false)
const [selectedProject, setSelectedProject] = useState<PsProject | null>(null)
// Modal functions
const openPhasesModal = (project: PsProject) => {
setSelectedProject(project)
setPhasesModalOpen(true)
}
const openTasksModal = (project: PsProject) => {
setSelectedProject(project)
setTasksModalOpen(true)
}
const closeModals = () => {
setPhasesModalOpen(false)
setTasksModalOpen(false)
setSelectedProject(null)
}
const {
data: projects,
isLoading,
error,
} = useQuery({
queryKey: ['projects', searchTerm, filterStatus, filterPriority],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500))
return mockProjects.filter((project) => {
const matchesSearch =
project.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.name.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = filterStatus === 'all' || project.status === filterStatus
const matchesPriority = filterPriority === 'all' || project.priority === filterPriority
return matchesSearch && matchesStatus && matchesPriority
})
},
})
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Projeler yükleniyor...</span>
</div>
)
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800">Projeler yüklenirken hata oluştu.</span>
</div>
</div>
)
}
return (
<Container>
<div className="space-y-2">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Proje Listesi</h2>
<p className="text-gray-600">Proje listesinizi yönetin</p>
</div>
<div className="flex items-center space-x-2">
{/* View Toggle */}
<div className="flex bg-gray-100 rounded-lg">
<button
onClick={() => setViewMode('card')}
className={`px-2.5 py-1.5 rounded-md flex items-center space-x-2 transition-colors ${
viewMode === 'card'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaTh className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`px-2.5 py-1.5 rounded-md flex items-center space-x-2 transition-colors ${
viewMode === 'list'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaList className="w-4 h-4" />
</button>
</div>
<button
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
className="flex items-center px-3 py-1.5 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
<FaDownload size={16} className="mr-2" />
Dışa Aktar
</button>
<Link
to={ROUTES_ENUM.protected.projects.new}
className="flex items-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={16} className="mr-2" />
Yeni Proje
</Link>
</div>
</div>
{/* Header Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center space-x-4">
<div className="relative">
<FaSearch
size={20}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
/>
<input
type="text"
placeholder="Proje kodu veya adı..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-3 py-1.5 text-sm w-80 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={classNames(
'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
showFilters
? 'border-blue-500 bg-blue-50 text-blue-700'
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
)}
>
<FaFilter size={16} className="mr-2" />
Filtreler
</button>
</div>
</div>
{/* Filters Panel */}
{showFilters && (
<div className="bg-white border border-gray-200 rounded-lg p-3">
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Durum</label>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
{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-2">Öncelik</label>
<select
value={filterPriority}
onChange={(e) => setFilterPriority(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-1.5 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
{Object.values(PriorityEnum).map((priority) => (
<option key={priority} value={priority}>
{getPriorityText(priority)}
</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={() => {
setFilterStatus('all')
setFilterPriority('all')
setSearchTerm('')
}}
className="w-full px-3 py-1.5 border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Filtreleri Temizle
</button>
</div>
</div>
</div>
)}
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Widget title="Toplam Proje" value={projects?.length || 0} color="blue" icon="FaFolder" />
<Widget
title="Aktif Proje"
value={projects?.filter((p) => p.status === ProjectStatusEnum.Active).length || 0}
color="green"
icon="FaArrowUp"
/>
<Widget
title="Toplam Bütçe"
value={`${projects?.reduce((acc, p) => acc + p.budget, 0).toLocaleString() || 0}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Ortalama İlerleme"
value={
projects?.length
? `${Math.round(
projects.reduce((acc, p) => acc + p.progress, 0) / projects.length,
)}%`
: '0%'
}
color="yellow"
icon="FaBullseye"
/>
</div>
{/* Projects Display */}
{viewMode === 'list' ? (
/* List View */
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="px-3 py-2 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Proje Listesi</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Proje Bilgileri
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Proje Yöneticisi
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Durum / Öncelik
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tarihler
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Bütçe / Maliyet
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
İlerleme
</th>
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
İşlemler
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{projects?.map((project) => (
<tr key={project.id} className="hover:bg-gray-50 transition-colors text-sm">
<td className="px-3 py-2">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<FaFolder className="h-5 w-5 text-blue-600" />
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{project.code}</div>
<div className="text-sm text-gray-500 max-w-xs truncate">
{project.name}
</div>
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="flex items-center text-sm">
<FaUser size={16} className="text-gray-400 mr-2" />
<div>
<div className="font-medium text-gray-900">
{project.projectManager?.fullName}
</div>
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="space-y-2">
<span
className={classNames(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
getProjectStatusColor(project.status),
)}
>
{getProjectStatusIcon(project.status)}
<span className="ml-1">{getProjectStatusText(project.status)}</span>
</span>
<div
className={classNames(
'text-sm font-medium',
getPriorityColor(project.priority),
)}
>
{getPriorityText(project.priority)}
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="space-y-1">
<div className="flex items-center text-sm text-gray-900">
<FaCalendar size={14} className="mr-1" />
{dayjs(project.startDate).format('DD.MM.YYYY')}
</div>
<div className="text-sm text-gray-500">
Bitiş: {dayjs(project.endDate).format('DD.MM.YYYY')}
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="space-y-1">
<div className="text-sm font-medium text-gray-900">
{project.budget.toLocaleString()}
</div>
<div className="text-sm text-gray-500">
Harcanan: {project.actualCost.toLocaleString()}
</div>
<div className="text-xs text-blue-600">
%{Math.round((project.actualCost / project.budget) * 100)} kullanıldı
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">
%{project.progress}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={classNames(
'h-2 rounded-full',
getProgressColor(project.progress),
)}
style={{ width: `${project.progress}%` }}
/>
</div>
</div>
</td>
<td className="px-3 py-2 text-right">
<div className="flex items-center justify-end space-x-1">
<Link
to={ROUTES_ENUM.protected.projects.detail.replace(':id', project.id)}
className="p-1.5 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Detayları Görüntüle"
>
<FaEye size={16} />
</Link>
<Link
to={ROUTES_ENUM.protected.projects.edit.replace(':id', project.id)}
className="p-1.5 text-gray-600 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit size={16} />
</Link>
<button
onClick={() => openPhasesModal(project)}
className="p-1.5 text-gray-600 hover:text-green-600 hover:bg-green-50 rounded-lg transition-colors"
title="Fazları Görüntüle"
>
<FaFlag size={16} />
</button>
<button
onClick={() => openTasksModal(project)}
className="p-1.5 text-gray-600 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
title="Görevleri Görüntüle"
>
<FaTasks size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
/* Card View */
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects?.map((project) => (
<div
key={project.id}
className="bg-white rounded-lg shadow-md border border-gray-200 p-4 hover:shadow-lg transition-shadow"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<FaFolder className="w-5 h-5 text-blue-600" />
<h3 className="text-base font-semibold text-gray-900">{project.code}</h3>
</div>
<p className="text-sm text-gray-600 mb-2">{project.name}</p>
<div className="flex items-center space-x-2 mb-3">
<span
className={classNames(
'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium',
getProjectStatusColor(project.status),
)}
>
{getProjectStatusIcon(project.status)}
<span className="ml-1">{getProjectStatusText(project.status)}</span>
</span>
<span
className={classNames(
'px-2 py-0.5 rounded-full text-xs font-medium',
getPriorityColor(project.priority),
)}
>
{getPriorityText(project.priority)}
</span>
</div>
</div>
<div className="flex space-x-1">
<Link
to={ROUTES_ENUM.protected.projects.detail.replace(':id', project.id)}
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-colors"
>
<FaEye className="w-4 h-4" />
</Link>
<Link
to={ROUTES_ENUM.protected.projects.edit.replace(':id', project.id)}
className="p-1.5 text-gray-400 hover:text-yellow-600 hover:bg-yellow-50 rounded-md transition-colors"
>
<FaEdit className="w-4 h-4" />
</Link>
<button
onClick={() => openPhasesModal(project)}
className="p-1.5 text-gray-400 hover:text-green-600 hover:bg-green-50 rounded-md transition-colors"
title="Fazları Görüntüle"
>
<FaFlag className="w-4 h-4" />
</button>
<button
onClick={() => openTasksModal(project)}
className="p-1.5 text-gray-400 hover:text-purple-600 hover:bg-purple-50 rounded-md transition-colors"
title="Görevleri Görüntüle"
>
<FaTasks className="w-4 h-4" />
</button>
</div>
</div>
{/* Project Manager */}
<div className="bg-gray-50 rounded-lg p-2 mb-3">
<div className="flex items-center space-x-2 mb-1">
<FaUser className="w-4 h-4 text-gray-600" />
<span className="font-medium text-gray-900">Proje Yöneticisi</span>
</div>
<p className="text-sm text-gray-700">{project.projectManager?.fullName}</p>
</div>
{/* Progress */}
<div className="mb-3">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-gray-500 flex items-center">
<FaBullseye className="w-4 h-4 mr-1" />
İlerleme
</span>
<span className="font-medium">{project.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={classNames('h-2 rounded-full', getProgressColor(project.progress))}
style={{ width: `${project.progress}%` }}
/>
</div>
</div>
{/* Dates and Budget */}
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500 flex items-center">
<FaCalendar className="w-4 h-4 mr-1" />
Başlangıç
</span>
<span className="font-medium">
{dayjs(project.startDate).format('DD.MM.YYYY')}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">Bitiş</span>
<span className="font-medium">
{dayjs(project.endDate).format('DD.MM.YYYY')}
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500 flex items-center">
<FaDollarSign className="w-4 h-4 mr-1" />
Bütçe
</span>
<span className="font-medium">{project.budget.toLocaleString()}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">Harcanan</span>
<span className="font-medium">{project.actualCost.toLocaleString()}</span>
</div>
</div>
</div>
{/* Budget Usage */}
<div className="bg-blue-50 rounded-lg p-2">
<div className="flex items-center justify-between text-sm">
<span className="text-blue-700 font-medium">Bütçe Kullanımı</span>
<span className="text-blue-600 font-bold">
%{Math.round((project.actualCost / project.budget) * 100)}
</span>
</div>
</div>
</div>
))}
</div>
)}
{/* Empty State */}
{(!projects || projects.length === 0) && (
<div className="text-center py-10">
<FaFolder className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">Proje bulunamadı</h3>
<p className="mt-1 text-sm text-gray-500">Yeni proje oluşturarak başlayın.</p>
<div className="mt-6">
<Link
to={ROUTES_ENUM.protected.projects.new}
className="inline-flex items-center px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={16} className="mr-2" />
Yeni Proje Oluştur
</Link>
</div>
</div>
)}
</div>
{/* Modals */}
{selectedProject && (
<>
<PhaseViewModal
project={selectedProject}
isOpen={phasesModalOpen}
onClose={closeModals}
/>
<TaskViewModal project={selectedProject} isOpen={tasksModalOpen} onClose={closeModals} />
</>
)}
</Container>
)
}
export default ProjectList