erp-platform/ui/src/views/hr/components/JobPositions.tsx
2025-10-29 13:20:21 +03:00

508 lines
18 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 {
FaBriefcase,
FaPlus,
FaEdit,
FaTrash,
FaUsers,
FaDollarSign,
FaBuilding,
FaEye,
FaTh,
FaList,
} from 'react-icons/fa'
import { JobPositionDto, JobLevelEnum } from '../../../types/hr'
import DataTable, { Column } from '../../../components/common/DataTable'
import { mockJobPositions } from '../../../mocks/mockJobPositions'
import JobPositionFormModal from './JobPositionFormModal'
import JobPositionViewModal from './JobPositionViewModal'
import { getJobLevelColor, getJobLevelText } from '../../../utils/erp'
import { Container } from '@/components/shared'
const JobPositions: React.FC = () => {
const [positions, setPositions] = useState<JobPositionDto[]>(mockJobPositions)
const [searchTerm, setSearchTerm] = useState('')
const [selectedLevel, setSelectedLevel] = useState<string>('all')
const [selectedDepartment, setSelectedDepartment] = useState<string>('all')
const [viewMode, setViewMode] = useState<'list' | 'card'>('list')
// Modal states
const [isFormModalOpen, setIsFormModalOpen] = useState(false)
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
const [selectedPosition, setSelectedPosition] = useState<JobPositionDto | undefined>(undefined)
const [modalTitle, setModalTitle] = useState('')
const handleAdd = () => {
setSelectedPosition(undefined)
setModalTitle('Yeni İş Pozisyonu')
setIsFormModalOpen(true)
}
const handleEdit = (position: JobPositionDto) => {
setSelectedPosition(position)
setModalTitle('İş Pozisyonu Düzenle')
setIsFormModalOpen(true)
}
const handleView = (position: JobPositionDto) => {
setSelectedPosition(position)
setIsViewModalOpen(true)
}
const handleDelete = (id: string) => {
if (window.confirm('Bu pozisyonu silmek istediğinizden emin misiniz?')) {
setPositions(positions.filter((p) => p.id !== id))
}
}
const handleSavePosition = (positionData: Partial<JobPositionDto>) => {
if (selectedPosition) {
// Edit existing position
const updatedPosition = {
...selectedPosition,
...positionData,
lastModificationTime: new Date(),
}
setPositions(positions.map((p) => (p.id === selectedPosition.id ? updatedPosition : p)))
} else {
// Add new position
const newPosition: JobPositionDto = {
id: `jp-${Date.now()}`,
...positionData,
employees: [],
creationTime: new Date(),
lastModificationTime: new Date(),
} as JobPositionDto
setPositions([...positions, newPosition])
}
}
const closeFormModal = () => {
setIsFormModalOpen(false)
setSelectedPosition(undefined)
setModalTitle('')
}
const closeViewModal = () => {
setIsViewModalOpen(false)
setSelectedPosition(undefined)
}
const filteredPositions = positions.filter((position) => {
if (
searchTerm &&
!position.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!position.code.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false
}
if (selectedLevel !== 'all' && position.level !== selectedLevel) {
return false
}
if (selectedDepartment !== 'all' && position.department?.id !== selectedDepartment) {
return false
}
return true
})
// Card component for individual position
const PositionCard: React.FC<{ position: JobPositionDto }> = ({ position }) => (
<div className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow p-4">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-base font-semibold text-gray-900">{position.name}</h3>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getJobLevelColor(
position.level,
)}`}
>
{getJobLevelText(position.level)}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">{position.code}</p>
<p
className="text-sm text-gray-700 overflow-hidden"
style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{position.description}
</p>
</div>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
position.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{position.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
<div className="space-y-2 mb-3">
<div className="flex items-center gap-2 text-sm text-gray-600">
<FaBuilding className="w-4 h-4" />
<span>{position.department?.name || 'Departman belirtilmemiş'}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<FaUsers className="w-4 h-4" />
<span>{position.employees?.length || 0} personel</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<FaDollarSign className="w-4 h-4" />
<span>
{position.minSalary.toLocaleString()} - {position.maxSalary.toLocaleString()}
</span>
</div>
</div>
<div className="mb-3">
<p className="text-xs font-medium text-gray-500 mb-2">Gerekli Yetenekler</p>
<div className="flex flex-wrap gap-1">
{position.requiredSkills?.slice(0, 4).map((skill, index) => (
<span key={index} className="px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded">
{skill}
</span>
))}
{position.requiredSkills?.length > 4 && (
<span className="text-xs text-gray-500 px-2 py-1">
+{position.requiredSkills.length - 4} daha
</span>
)}
</div>
</div>
<div className="flex gap-1.5">
<button
onClick={() => handleView(position)}
className="flex-1 flex items-center justify-center gap-1 px-2 py-1.5 text-xs text-green-700 bg-green-50 hover:bg-green-100 rounded-md transition-colors"
>
<FaEye className="w-4 h-4" />
Görüntüle
</button>
<button
onClick={() => handleEdit(position)}
className="flex-1 flex items-center justify-center gap-1 px-2 py-1.5 text-xs text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
>
<FaEdit className="w-4 h-4" />
Düzenle
</button>
<button
onClick={() => handleDelete(position.id)}
className="px-2 py-1.5 text-xs text-red-700 bg-red-50 hover:bg-red-100 rounded-md transition-colors"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
)
const columns: Column<JobPositionDto>[] = [
{
key: 'code',
header: 'Pozisyon Kodu',
sortable: true,
},
{
key: 'title',
header: 'Pozisyon Adı',
sortable: true,
render: (position: JobPositionDto) => (
<div>
<div className="font-medium text-gray-900">{position.name}</div>
<div className="text-sm text-gray-500 truncate max-w-xs">{position.description}</div>
</div>
),
},
{
key: 'department',
header: 'Departman',
render: (position: JobPositionDto) => (
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-500" />
<span>{position.department?.name || '-'}</span>
</div>
),
},
{
key: 'level',
header: 'Seviye',
render: (position: JobPositionDto) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getJobLevelColor(
position.level,
)}`}
>
{getJobLevelText(position.level)}
</span>
),
},
{
key: 'employeeCount',
header: 'Personel Sayısı',
render: (position: JobPositionDto) => (
<div className="flex items-center gap-1">
<FaUsers className="w-4 h-4 text-gray-500" />
<span>{position.employees?.length || 0}</span>
</div>
),
},
{
key: 'salary',
header: 'Maaş Aralığı',
render: (position: JobPositionDto) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-500" />
<div className="text-sm">
<div>{position.minSalary.toLocaleString()}</div>
<div className="text-gray-500">{position.maxSalary.toLocaleString()}</div>
</div>
</div>
),
},
{
key: 'skills',
header: 'Gerekli Yetenekler',
render: (position: JobPositionDto) => (
<div className="flex flex-wrap gap-1 max-w-xs">
{position.requiredSkills?.slice(0, 3).map((skill, index) => (
<span key={index} className="px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded">
{skill}
</span>
))}
{position.requiredSkills?.length > 3 && (
<span className="text-xs text-gray-500">
+{position.requiredSkills.length - 3} daha
</span>
)}
</div>
),
},
{
key: 'status',
header: 'Durum',
render: (position: JobPositionDto) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
position.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{position.isActive ? 'Aktif' : 'Pasif'}
</span>
),
},
{
key: 'actions',
header: 'İşlemler',
render: (position: JobPositionDto) => (
<div className="flex gap-2">
<button
onClick={() => handleView(position)}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(position)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(position.id)}
className="p-1 text-red-600 hover:bg-red-50 rounded"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
),
},
]
// Get unique departments for filter
const departments = [...new Set(positions.map((p) => p.department).filter(Boolean))]
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">
<div>
<h2 className="text-2xl font-bold text-gray-900">İş Pozisyonları</h2>
<p className="text-gray-600">Şirket pozisyonları ve tanımları yönetimi</p>
</div>
<div className="flex items-center gap-3">
{/* View Toggle */}
<div className="flex bg-gray-100 rounded-lg p-0.5">
<button
onClick={() => setViewMode('list')}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'list'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<FaList className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('card')}
className={`flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors ${
viewMode === 'card'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-500 hover:text-gray-700'
}`}
>
<FaTh className="w-4 h-4" />
</button>
</div>
<button
onClick={handleAdd}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<FaPlus className="w-4 h-4" />
<span className="hidden sm:inline">Yeni Pozisyon</span>
<span className="sm:hidden">Yeni</span>
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaBriefcase className="w-6 h-6 text-blue-600" />
<div className="ml-3">
<p className="text-xs font-medium text-gray-600">Toplam Pozisyon</p>
<p className="text-xl font-bold text-gray-900">{positions.length}</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaUsers className="w-6 h-6 text-green-600" />
<div className="ml-3">
<p className="text-xs font-medium text-gray-600">Dolu Pozisyonlar</p>
<p className="text-xl font-bold text-gray-900">
{positions.filter((p) => p.employees && p.employees.length > 0).length}
</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaBriefcase className="w-6 h-6 text-orange-600" />
<div className="ml-3">
<p className="text-xs font-medium text-gray-600">Boş Pozisyonlar</p>
<p className="text-xl font-bold text-gray-900">
{positions.filter((p) => !p.employees || p.employees.length === 0).length}
</p>
</div>
</div>
</div>
<div className="bg-white p-4 rounded-lg shadow-sm border">
<div className="flex items-center">
<FaBuilding className="w-6 h-6 text-purple-600" />
<div className="ml-3">
<p className="text-xs font-medium text-gray-600">Departman Sayısı</p>
<p className="text-xl font-bold text-gray-900">{departments.length}</p>
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="flex-1">
<input
type="text"
placeholder="Pozisyon adı veya kodu ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<select
value={selectedLevel}
onChange={(e) => setSelectedLevel(e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[160px]"
>
<option value="all">Tüm Seviyeler</option>
{Object.values(JobLevelEnum).map((level) => (
<option key={level} value={level}>
{getJobLevelText(level)}
</option>
))}
</select>
<select
value={selectedDepartment}
onChange={(e) => setSelectedDepartment(e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[160px]"
>
<option value="all">Tüm Departmanlar</option>
{departments.map((dept) => (
<option key={dept?.id} value={dept?.id}>
{dept?.name}
</option>
))}
</select>
</div>
</div>
{/* Content - List or Card View */}
{viewMode === 'list' ? (
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="overflow-x-auto">
<DataTable data={filteredPositions} columns={columns} />
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredPositions.map((position) => (
<PositionCard key={position.id} position={position} />
))}
</div>
)}
{/* Empty State */}
{filteredPositions.length === 0 && (
<div className="text-center py-12">
<FaBriefcase className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-2">Pozisyon bulunamadı</h3>
<p className="text-sm text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
</div>
{/* Modals */}
<JobPositionFormModal
isOpen={isFormModalOpen}
onClose={closeFormModal}
onSave={handleSavePosition}
position={selectedPosition}
title={modalTitle}
/>
<JobPositionViewModal
isOpen={isViewModalOpen}
onClose={closeViewModal}
position={selectedPosition}
/>
</Container>
)
}
export default JobPositions