erp-platform/ui/src/views/hr/components/EmployeeList.tsx
2025-09-18 10:24:36 +03:00

559 lines
23 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 {
FaUsers,
FaPlus,
FaSearch,
FaFilter,
FaDownload,
FaEdit,
FaEye,
FaPhone,
FaEnvelope,
FaCalendar,
FaBuilding,
FaAward,
FaExclamationTriangle,
FaList,
FaTh,
FaBriefcase,
} from 'react-icons/fa'
import classNames from 'classnames'
import { EmployeeStatusEnum, HrEmployee } from '../../../types/hr'
import dayjs from 'dayjs'
import { mockEmployees } from '../../../mocks/mockEmployees'
import EmployeeView from './EmployeeView'
import Widget from '../../../components/common/Widget'
import {
getEmploymentTypeColor,
getEmploymentTypeText,
getEmployeeStatusColor,
getEmployeeStatusIcon,
getEmployeeStatusText,
} from '../../../utils/erp'
import { Container } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { mockDepartments } from '@/mocks/mockDepartments'
const EmployeeList: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState('all')
const [filterDepartment, setFilterDepartment] = useState('all')
const [showFilters, setShowFilters] = useState(false)
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
// Modal states
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
const [selectedEmployee, setSelectedEmployee] = useState<HrEmployee | null>(null)
const {
data: employees,
isLoading,
error,
} = useQuery({
queryKey: ['employees', searchTerm, filterStatus, filterDepartment],
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 500))
return mockEmployees.filter((employee) => {
const matchesSearch =
employee.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.fullName.toLowerCase().includes(searchTerm.toLowerCase()) ||
employee.email.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = filterStatus === 'all' || employee.employeeStatus === filterStatus
const matchesDepartment =
filterDepartment === 'all' || employee.department?.code === filterDepartment
return matchesSearch && matchesStatus && matchesDepartment
})
},
})
// Modal handlers
const handleViewEmployee = (employee: HrEmployee) => {
setSelectedEmployee(employee)
setIsViewModalOpen(true)
}
const handleCloseViewModal = () => {
setIsViewModalOpen(false)
setSelectedEmployee(null)
}
const handleEditFromView = (employee: HrEmployee) => {
setIsViewModalOpen(false)
// Navigate to edit page - you can replace this with a modal if preferred
window.location.href = ROUTES_ENUM.protected.hr.employeesEdit.replace(':id', employee.id)
}
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">Personel listesi 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">Personel listesi yüklenirken hata oluştu.</span>
</div>
</div>
)
}
return (
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
{/* Title & Description */}
<div>
<h2 className="text-2xl font-bold text-gray-900">Personel Listesi</h2>
<p className="text-gray-600">Şirket çalışanlarının listesi</p>
</div>
{/* Header Actions */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
{/* View Mode Toggle */}
<div className="flex bg-gray-100 rounded-lg p-0.5">
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded-md transition-colors ${
viewMode === 'list'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
title="Liste Görünümü"
>
<FaList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('cards')}
className={`p-1.5 rounded-md transition-colors ${
viewMode === 'cards'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
title="Kart Görünümü"
>
<FaTh className="w-4 h-4" />
</button>
</div>
{/* Search Box */}
<div className="relative">
<FaSearch
size={18}
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
/>
<input
type="text"
placeholder="Personel kodu, ad veya e-posta..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-1 text-sm w-full sm:w-64 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Filter Button */}
<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>
{/* Export Button */}
<button
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
className="flex items-center px-3 py-1 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>
{/* Add New Employee */}
<Link
to={ROUTES_ENUM.protected.hr.employeesNew}
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 Personel
</Link>
</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-xs font-medium text-gray-700 mb-1">Durum</label>
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
{Object.values(EmployeeStatusEnum).map((status) => (
<option key={status} value={status}>
{getEmployeeStatusText(status)}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Departman</label>
<select
value={filterDepartment}
onChange={(e) => setFilterDepartment(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">Tümü</option>
{mockDepartments.map((dept) => (
<option key={dept.id} value={dept.code}>
{dept.name}
</option>
))}
</select>
</div>
<div className="flex items-end">
<button
onClick={() => {
setFilterStatus('all')
setFilterDepartment('all')
setSearchTerm('')
}}
className="w-full px-3 py-1 text-sm 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-3">
<Widget
title="Toplam Personel"
value={employees?.length || 0}
color="blue"
icon="FaUsers"
/>
<Widget
title="Aktif Personel"
value={
employees?.filter((e) => e.employeeStatus === EmployeeStatusEnum.Active).length || 0
}
color="green"
icon="FaCheckCircle"
/>
<Widget
title="İzinli Personel"
value={
employees?.filter((e) => e.employeeStatus === EmployeeStatusEnum.OnLeave).length || 0
}
color="yellow"
icon="FaCalendar"
/>
<Widget
title="Yeni İşe Alınanlar"
value={
employees?.filter((e) => dayjs(e.hireDate).isAfter(dayjs().subtract(30, 'day')))
.length || 0
}
color="purple"
icon="FaArrowUp"
/>
</div>
{/* Employees Display */}
{viewMode === 'list' ? (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 text-xs">
<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">
Personel Bilgileri
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
İletişim
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pozisyon / Departman
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
İstihdam Bilgileri
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Maaş
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Durum
</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">
{employees?.map((employee) => (
<tr key={employee.id} className="hover:bg-gray-50 transition-colors text-xs">
<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-full bg-blue-100 flex items-center justify-center">
<FaUsers className="h-5 w-5 text-blue-600" />
</div>
</div>
<div className="ml-4">
<div className="text-xs font-medium text-gray-900">{employee.code}</div>
<div className="text-xs text-gray-500">{employee.fullName}</div>
{employee.badgeNumber && (
<div className="text-xs text-gray-400 mt-1">
Rozet: {employee.badgeNumber}
</div>
)}
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="space-y-1">
<div className="flex items-center text-xs text-gray-900">
<FaEnvelope size={14} className="mr-1" />
{employee.email}
</div>
{employee.phone && (
<div className="flex items-center text-xs text-gray-500">
<FaPhone size={14} className="mr-1" />
{employee.phone}
</div>
)}
</div>
</td>
<td className="px-3 py-2">
<div>
<div className="text-xs font-medium text-gray-900">
{employee.jobPosition?.name}
</div>
<div className="flex items-center text-xs text-gray-500">
<FaBuilding size={14} className="mr-1" />
{employee.department?.name}
</div>
<div className="text-xs text-gray-400 mt-1">{employee.workLocation}</div>
</div>
</td>
<td className="px-3 py-2">
<div className="space-y-1">
<div
className={classNames(
'text-xs font-medium',
getEmploymentTypeColor(employee.employmentType),
)}
>
{getEmploymentTypeText(employee.employmentType)}
</div>
<div className="flex items-center text-xs text-gray-500">
<FaCalendar size={14} className="mr-1" />
{dayjs(employee.hireDate).format('DD.MM.YYYY')}
</div>
<div className="text-xs text-gray-400">
{dayjs().diff(employee.hireDate, 'year')} yıl deneyim
</div>
</div>
</td>
<td className="px-3 py-2">
<div className="text-xs font-medium text-gray-900">
{employee.baseSalary.toLocaleString()}
</div>
<div className="text-xs text-gray-500">{employee.payrollGroup}</div>
</td>
<td className="px-3 py-2">
<span
className={classNames(
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
getEmployeeStatusColor(employee.employeeStatus),
)}
>
{getEmployeeStatusIcon(employee.employeeStatus)}
<span className="ml-1">
{getEmployeeStatusText(employee.employeeStatus)}
</span>
</span>
</td>
<td className="px-3 py-2 text-right">
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => handleViewEmployee(employee)}
className="p-1 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Detayları Görüntüle"
>
<FaEye size={16} />
</button>
<Link
to={ROUTES_ENUM.protected.hr.employeesEdit.replace(':id', employee.id)}
className="p-1 text-gray-600 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit size={16} />
</Link>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{(!employees || employees.length === 0) && (
<div className="text-center py-12">
<FaUsers className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-xs font-medium text-gray-900">Personel bulunamadı</h3>
<p className="mt-1 text-xs text-gray-500">Yeni personel ekleyerek başlayın.</p>
<div className="mt-6">
<Link
to={ROUTES_ENUM.protected.hr.employeesNew}
className="inline-flex items-center px-3 py-1 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus size={16} className="mr-2" />
Yeni Personel Ekle
</Link>
</div>
</div>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{employees?.map((employee) => (
<div
key={employee.id}
className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow p-3"
>
{/* Card Header */}
<div className="flex items-start space-x-4 mb-4">
<div className="flex-shrink-0">
<div className="h-12 w-12 rounded-full bg-blue-500 flex items-center justify-center">
<span className="text-lg font-medium text-white">
{employee.firstName.charAt(0)}
{employee.lastName.charAt(0)}
</span>
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-gray-900 truncate">
{employee.fullName}
</h3>
<p className="text-sm text-gray-500">{employee.code}</p>
<span
className={classNames(
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mt-1',
getEmployeeStatusColor(employee.employeeStatus),
)}
>
{getEmployeeStatusIcon(employee.employeeStatus)}
<span className="ml-1">{getEmployeeStatusText(employee.employeeStatus)}</span>
</span>
</div>
</div>
{/* Card Content */}
<div className="space-y-1.5">
<div className="flex items-center text-sm text-gray-600">
<FaBriefcase className="w-4 h-4 mr-2 text-gray-400" />
<span>{employee.jobPosition?.name || 'Pozisyon belirtilmemiş'}</span>
</div>
<div className="flex items-center text-sm text-gray-600">
<FaBuilding className="w-4 h-4 mr-2 text-gray-400" />
<span>{employee.department?.name || 'Departman belirtilmemiş'}</span>
</div>
<div className="flex items-center text-sm text-gray-600">
<FaEnvelope className="w-4 h-4 mr-2 text-gray-400" />
<span className="truncate">{employee.email}</span>
</div>
{employee.phone && (
<div className="flex items-center text-sm text-gray-600">
<FaPhone className="w-4 h-4 mr-2 text-gray-400" />
<span>{employee.phone}</span>
</div>
)}
<div className="flex items-center text-sm text-gray-600">
<FaCalendar className="w-4 h-4 mr-2 text-gray-400" />
<span>İşe Başlama: {dayjs(employee.hireDate).format('DD.MM.YYYY')}</span>
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">İstihdam Türü:</span> {employee.employmentType}
</div>
</div>
{/* Card Actions */}
<div className="flex justify-end space-x-1 mt-2 pt-2 border-t border-gray-100">
<button
onClick={() => handleViewEmployee(employee)}
className="p-1 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
title="Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
<Link
to={ROUTES_ENUM.protected.hr.employeesEdit.replace(':id', employee.id)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</Link>
<button
onClick={() => alert('Performans değerlendirme özelliği yakında eklenecek')}
className="p-1 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
title="Performans"
>
<FaAward className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Employee View Modal */}
<EmployeeView
isOpen={isViewModalOpen}
onClose={handleCloseViewModal}
employee={selectedEmployee}
onEdit={handleEditFromView}
/>
</Container>
)
}
export default EmployeeList