erp-platform/ui/src/views/hr/components/OrganizationChart.tsx
2025-09-16 00:02:48 +03:00

644 lines
24 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, useEffect, useMemo } from 'react'
import {
FaUser,
FaUsers,
FaBuilding,
FaChevronDown,
FaChevronRight,
FaEye,
FaTh,
FaSitemap,
FaTimes,
FaCalendar,
FaEnvelope,
FaPhone,
FaMapMarkerAlt,
FaBriefcase,
} from 'react-icons/fa'
import { HrEmployee, HrOrganizationChart as OrgChart } from '../../../types/hr'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { mockDepartments } from '../../../mocks/mockDepartments'
import Widget from '../../../components/common/Widget'
import { Container } from '@/components/shared'
// Dinamik organizasyon verisi oluşturma fonksiyonu
const generateOrganizationData = (): OrgChart[] => {
const orgData: OrgChart[] = []
// Çalışanları işle
mockEmployees.forEach((employee) => {
const department = mockDepartments.find((d) => d.id === employee.departmantId)
let level = 3 // Varsayılan seviye (normal çalışan)
let parentId: string | undefined = undefined
// Eğer bu çalışan bir departman yöneticisiyse
if (department && department.managerId === employee.id) {
if (!department.parentDepartmentId) {
// Ana departman yöneticisi (CEO/Genel Müdür)
level = 0
} else {
// Alt departman yöneticisi
level = 1
// Üst departmanın yöneticisini parent olarak bul
const parentDept = mockDepartments.find((d) => d.id === department.parentDepartmentId)
if (parentDept && parentDept.managerId) {
parentId = parentDept.managerId
}
}
} else {
// Normal çalışan - departman yöneticisine bağlı
if (department && department.managerId) {
parentId = department.managerId
// Departman yöneticisinin seviyesine göre belirleme
const managerDept = mockDepartments.find((d) => d.managerId === department.managerId)
if (managerDept && !managerDept.parentDepartmentId) {
level = 2 // CEO'ya direkt bağlı
} else {
level = 3 // Orta kademe yöneticiye bağlı
}
}
}
orgData.push({
id: `org-${employee.id}`,
employeeId: employee.id,
employee: employee,
parentId: parentId,
level: level,
position: employee.jobPosition?.name || 'Belirsiz',
isActive: employee.isActive,
})
})
return orgData.sort((a, b) => a.level - b.level)
}
interface TreeNode {
employee: HrEmployee
children: TreeNode[]
level: number
}
const OrganizationChart: React.FC = () => {
const [employees] = useState<HrEmployee[]>(mockEmployees)
const [organizationData] = useState<OrgChart[]>(generateOrganizationData())
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
const [orgTree, setOrgTree] = useState<TreeNode[]>([])
const [viewMode, setViewMode] = useState<'tree' | 'cards'>('tree')
const [selectedEmployee, setSelectedEmployee] = useState<HrEmployee | null>(null)
const [showModal, setShowModal] = useState(false)
const [selectedDepartment, setSelectedDepartment] = useState<string>('all')
const handleViewEmployee = (employee: HrEmployee) => {
setSelectedEmployee(employee)
setShowModal(true)
}
const handleCloseModal = () => {
setShowModal(false)
setSelectedEmployee(null)
}
useEffect(() => {
// Build organization tree structure
const buildTree = (): TreeNode[] => {
const nodeMap = new Map<string, TreeNode>()
// First, create all nodes
employees.forEach((employee) => {
nodeMap.set(employee.id, {
employee,
children: [],
level: 0,
})
})
// Then, build the tree structure
const rootNodes: TreeNode[] = []
organizationData.forEach((orgData) => {
const node = nodeMap.get(orgData.employeeId)
if (node) {
node.level = orgData.level
if (orgData.parentId) {
const parentNode = nodeMap.get(orgData.parentId)
if (parentNode) {
parentNode.children.push(node)
}
} else {
rootNodes.push(node)
}
}
})
return rootNodes
}
setOrgTree(buildTree())
// Auto-expand first level
const firstLevelIds = organizationData
.filter((org) => org.level <= 1)
.map((org) => org.employeeId)
setExpandedNodes(new Set(firstLevelIds))
}, [employees, organizationData])
// Filtered data for views
const filteredOrgTree = useMemo(() => {
if (selectedDepartment === 'all') {
return orgTree
}
const filterTree = (nodes: TreeNode[]): TreeNode[] => {
const result: TreeNode[] = []
for (const node of nodes) {
const filteredChildren = filterTree(node.children)
if (node.employee.departmantId === selectedDepartment || filteredChildren.length > 0) {
result.push({ ...node, children: filteredChildren })
}
}
return result
}
return filterTree(orgTree)
}, [orgTree, selectedDepartment])
const filteredEmployees = useMemo(() => {
if (selectedDepartment === 'all') {
return employees
}
return employees.filter((emp) => emp.departmantId === selectedDepartment)
}, [employees, selectedDepartment])
const toggleNode = (nodeId: string) => {
const newExpanded = new Set(expandedNodes)
if (newExpanded.has(nodeId)) {
newExpanded.delete(nodeId)
} else {
newExpanded.add(nodeId)
}
setExpandedNodes(newExpanded)
}
const renderTreeNode = (node: TreeNode): React.ReactNode => {
const isExpanded = expandedNodes.has(node.employee.id)
const hasChildren = node.children.length > 0
return (
<div key={node.employee.id} className="relative">
{/* Bağlantı çizgisi */}
{node.level > 0 && (
<span
className="absolute border-l border-gray-300"
style={{
left: `${node.level * 1.25 - 0.7}rem`,
top: 0,
height: '100%',
}}
/>
)}
<div
className="flex items-center space-x-2 py-1.5 rounded-md hover:bg-gray-50"
style={{ paddingLeft: `${node.level * 1.25}rem` }}
>
{/* Girinti ve bağlantı çizgisi */}
{node.level > 0 && (
<span
className="absolute border-t border-gray-300 w-3"
style={{ left: `${node.level * 1.25 - 0.7}rem`, top: '1.1rem' }}
/>
)}
{/* Genişletme ikonu */}
<div className="w-5 flex-shrink-0 text-center">
{hasChildren && (
<button
onClick={() => toggleNode(node.employee.id)}
className="p-1 text-gray-500 hover:text-gray-800 rounded-full hover:bg-gray-200"
>
{isExpanded ? <FaChevronDown size={10} /> : <FaChevronRight size={10} />}
</button>
)}
</div>
{/* Grup Bilgisi */}
<div className="flex-grow flex items-center space-x-3">
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-sm text-gray-900 truncate">
{node.employee.fullName}
</h4>
<p className="text-xs text-gray-600 truncate">
{node.employee.jobPosition?.name}
</p>
<p className="text-xs text-gray-500 truncate">{node.employee.department?.name}</p>
</div>
</div>
</div>
<div className="flex items-center space-x-2 pr-2">
<div className="flex items-center gap-2 ml-4">
{hasChildren && (
<span className="flex items-center text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
<FaUsers className="w-3 h-3 mr-1" />
{node.children.length}
</span>
)}
<button // İşlemler
onClick={() => handleViewEmployee(node.employee)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
{hasChildren && isExpanded && (
<div>{node.children.map((child) => renderTreeNode(child))}</div>
)}
</div>
)
}
const renderCardView = () => {
const groupedByLevel = filteredEmployees.reduce(
(acc, employee) => {
const orgData = organizationData.find((org) => org.employeeId === employee.id)
const level = orgData?.level || 0
if (!acc[level]) {
acc[level] = []
}
acc[level].push(employee)
return acc
},
{} as Record<number, HrEmployee[]>,
)
return (
<div className="space-y-6">
{Object.entries(groupedByLevel)
.sort(([a], [b]) => parseInt(a) - parseInt(b))
.map(([level, levelEmployees]) => (
<div key={level}>
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<FaBuilding className="w-5 h-5" />
{level === '0'
? 'Üst Yönetim'
: level === '1'
? 'Orta Kademe Yönetim'
: level === '2'
? 'Alt Kademe Yönetim'
: `${parseInt(level) + 1}. Seviye`}
<span className="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
{levelEmployees.length} kişi
</span>
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 gap-3">
{levelEmployees.map((employee) => (
<div
key={employee.id}
className="bg-white p-3 rounded-lg border shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-center mb-3">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mr-2 ${
level === '0'
? 'bg-blue-100'
: level === '1'
? 'bg-green-100'
: 'bg-gray-100'
}`}
>
<FaUser
className={`w-4 h-4 ${
level === '0'
? 'text-blue-600'
: level === '1'
? 'text-green-600'
: 'text-gray-600'
}`}
/>
</div>
<div className="flex-1 min-w-0">
<h4
onClick={() => handleViewEmployee(employee)}
className="font-medium text-sm text-blue-600 hover:underline cursor-pointer truncate"
>
{employee.fullName}
</h4>
<p className="text-sm text-gray-600 truncate">{employee.code}</p>
</div>
</div>
<div className="space-y-1 mb-2">
<p className="text-xs text-gray-800 font-medium truncate">
{employee.jobPosition?.name}
</p>
<p className="text-xs text-gray-600 truncate">{employee.department?.name}</p>
</div>
</div>
))}
</div>
</div>
))}
</div>
)
}
return (
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">Organizasyon Şeması</h2>
<p className="text-gray-600">Kurumsal hiyerarşi ve raporlama yapısı</p>
</div>
<div className="flex items-center gap-2">
<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"
>
<option value="all">Tüm Departmanlar</option>
{mockDepartments.map((dept) => (
<option key={dept.id} value={dept.id}>
{dept.name}
</option>
))}
</select>
<div className="flex bg-gray-100 rounded-lg p-0.5">
<button
onClick={() => setViewMode('tree')}
className={`p-1.5 rounded-md transition-colors ${
viewMode === 'tree'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FaSitemap 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'
}`}
>
<FaTh className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Widget title="Toplam Personel" value={employees.length} color="blue" icon="FaBuilding" />
<Widget
title="Departmanlar"
value={new Set(employees.map((e) => e.department?.id).filter(Boolean)).size}
color="purple"
icon="FaBuilding"
/>
<Widget
title="Seviye Sayısı"
value={
organizationData.length > 0
? Math.max(...organizationData.map((org) => org.level)) + 1
: 0
}
color="orange"
icon="FaUser"
/>
</div>
{/* Content */}
<div className="bg-white rounded-lg border p-4">
{viewMode === 'tree' ? (
<div className="pt-2">{filteredOrgTree.map((node) => renderTreeNode(node))}</div>
) : (
renderCardView()
)}
{(viewMode === 'tree'
? filteredOrgTree.length === 0
: filteredEmployees.length === 0) && (
<div className="text-center py-12">
<FaBuilding className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-base font-medium text-gray-900 mb-2">
Organizasyon verisi bulunamadı
</h3>
<p className="text-gray-500">Henüz organizasyon şeması oluşturulmamış.</p>
</div>
)}
</div>
</div>
{/* Employee Detail Modal */}
{showModal && selectedEmployee && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg p-4 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
{/* Modal Header */}
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-900">Personel Detayları</h3>
<button onClick={handleCloseModal} className="text-gray-400 hover:text-gray-600 p-1">
<FaTimes className="w-5 h-5" />
</button>
</div>
{/* Employee Info */}
<div className="space-y-4">
{/* Basic Info */}
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
<FaUser className="w-8 h-8 text-blue-600" />
</div>
<div>
<h4 className="text-lg font-bold text-gray-900">{selectedEmployee.fullName}</h4>
<p className="text-gray-600">{selectedEmployee.code}</p>
<p className="text-sm text-gray-500">{selectedEmployee.jobPosition?.name}</p>
</div>
</div>
{/* Contact Information */}
<div>
<h5 className="text-base font-semibold text-gray-900 mb-2 flex items-center">
<FaEnvelope className="w-5 h-5 mr-2 text-blue-600" />
İletişim Bilgileri
</h5>
<div className="bg-gray-50 p-3 rounded-lg space-y-2">
<div className="flex items-center">
<FaEnvelope className="w-4 h-4 text-gray-500 mr-3" />
<span className="text-sm">E-posta:</span>
<span className="ml-2 text-sm font-medium">{selectedEmployee.email}</span>
</div>
{selectedEmployee.phone && (
<div className="flex items-center">
<FaPhone className="w-4 h-4 text-gray-500 mr-3" />
<span className="text-sm">İş Telefonu:</span>
<span className="ml-2 text-sm font-medium">{selectedEmployee.phone}</span>
</div>
)}
{selectedEmployee.personalPhone && (
<div className="flex items-center">
<FaPhone className="w-4 h-4 text-gray-500 mr-3" />
<span className="text-sm">Kişisel Telefon:</span>
<span className="ml-2 text-sm font-medium">
{selectedEmployee.personalPhone}
</span>
</div>
)}
</div>
</div>
{/* Employment Information */}
<div>
<h5 className="text-base font-semibold text-gray-900 mb-2 flex items-center">
<FaBriefcase className="w-5 h-5 mr-2 text-green-600" />
İş Bilgileri
</h5>
<div className="bg-gray-50 p-3 rounded-lg space-y-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span className="text-sm text-gray-600">Departman:</span>
<p className="font-medium">{selectedEmployee.department?.name}</p>
</div>
<div>
<span className="text-sm text-gray-600">Pozisyon:</span>
<p className="font-medium">{selectedEmployee.jobPosition?.name}</p>
</div>
<div>
<span className="text-sm text-gray-600">İşe Başlama Tarihi:</span>
<p className="font-medium flex items-center">
<FaCalendar className="w-4 h-4 mr-1 text-gray-500" />
{new Date(selectedEmployee.hireDate).toLocaleDateString('tr-TR')}
</p>
</div>
<div>
<span className="text-sm text-gray-600">İstihdam Türü:</span>
<p className="font-medium">{selectedEmployee.employmentType}</p>
</div>
<div>
<span className="text-sm text-gray-600">Çalışma Lokasyonu:</span>
<p className="font-medium flex items-center">
<FaMapMarkerAlt className="w-4 h-4 mr-1 text-gray-500" />
{selectedEmployee.workLocation}
</p>
</div>
<div>
<span className="text-sm text-gray-600">Durum:</span>
<span
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
selectedEmployee.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{selectedEmployee.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
</div>
</div>
</div>
{/* Personal Information */}
<div>
<h5 className="text-base font-semibold text-gray-900 mb-2 flex items-center">
<FaUser className="w-5 h-5 mr-2 text-purple-600" />
Kişisel Bilgiler
</h5>
<div className="bg-gray-50 p-3 rounded-lg space-y-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span className="text-sm text-gray-600">Doğum Tarihi:</span>
<p className="font-medium flex items-center">
<FaCalendar className="w-4 h-4 mr-1 text-gray-500" />
{new Date(selectedEmployee.birthDate).toLocaleDateString('tr-TR')}
</p>
</div>
<div>
<span className="text-sm text-gray-600">Cinsiyet:</span>
<p className="font-medium">{selectedEmployee.gender}</p>
</div>
<div>
<span className="text-sm text-gray-600">Medeni Durum:</span>
<p className="font-medium">{selectedEmployee.maritalStatus}</p>
</div>
<div>
<span className="text-sm text-gray-600">TC Kimlik No:</span>
<p className="font-medium">{selectedEmployee.nationalId}</p>
</div>
</div>
{selectedEmployee.address && (
<div>
<span className="text-sm text-gray-600">Adres:</span>
<p className="font-medium flex items-start">
<FaMapMarkerAlt className="w-4 h-4 mr-1 text-gray-500 mt-0.5" />
{selectedEmployee.address.street}, {selectedEmployee.address.city},{' '}
{selectedEmployee.address.country}
</p>
</div>
)}
</div>
</div>
{/* Emergency Contact */}
{selectedEmployee.emergencyContact && (
<div>
<h5 className="text-base font-semibold text-gray-900 mb-2 flex items-center">
<FaPhone className="w-5 h-5 mr-2 text-red-600" />
Acil Durum İletişim
</h5>
<div className="bg-gray-50 p-3 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<span className="text-sm text-gray-600">İsim:</span>
<p className="font-medium">{selectedEmployee.emergencyContact.name}</p>
</div>
<div>
<span className="text-sm text-gray-600">Yakınlık:</span>
<p className="font-medium">
{selectedEmployee.emergencyContact.relationship}
</p>
</div>
<div>
<span className="text-sm text-gray-600">Telefon:</span>
<p className="font-medium">{selectedEmployee.emergencyContact.phone}</p>
</div>
</div>
</div>
</div>
)}
</div>
{/* Modal Footer */}
<div className="flex justify-end gap-3 mt-4 pt-4 border-t">
<button
onClick={handleCloseModal}
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Kapat
</button>
</div>
</div>
</div>
)}
</Container>
)
}
export default OrganizationChart