erp-platform/ui/src/views/hr/components/DepartmentManagement.tsx

462 lines
16 KiB
TypeScript
Raw Normal View History

2025-09-15 19:46:52 +00:00
import React, { useState } from 'react'
2025-09-15 09:31:47 +00:00
import {
FaPlus,
FaEdit,
FaTrash,
FaBuilding,
FaUsers,
FaDollarSign,
FaEye,
FaList,
FaTh,
2025-09-15 19:46:52 +00:00
} from 'react-icons/fa'
import { HrDepartment } from '../../../types/hr'
import DataTable, { Column } from '../../../components/common/DataTable'
import { mockDepartments } from '../../../mocks/mockDepartments'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { mockCostCenters } from '../../../mocks/mockCostCenters'
import DepartmentFormModal from './DepartmentFormModal'
import DepartmentViewModal from './DepartmentViewModal'
import Widget from '../../../components/common/Widget'
import { Container } from '@/components/shared'
2025-09-15 09:31:47 +00:00
const DepartmentManagement: React.FC = () => {
2025-09-15 19:46:52 +00:00
const [departments, setDepartments] = useState<HrDepartment[]>(mockDepartments)
const [searchTerm, setSearchTerm] = useState('')
const [selectedParent, setSelectedParent] = useState<string>('all')
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
2025-09-15 09:31:47 +00:00
// Modal states
2025-09-15 19:46:52 +00:00
const [isFormModalOpen, setIsFormModalOpen] = useState(false)
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
const [selectedDepartment, setSelectedDepartment] = useState<HrDepartment | undefined>()
const [modalTitle, setModalTitle] = useState('')
2025-09-15 09:31:47 +00:00
const handleAdd = () => {
2025-09-15 19:46:52 +00:00
setSelectedDepartment(undefined)
setModalTitle('Yeni Departman')
setIsFormModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const handleEdit = (department: HrDepartment) => {
2025-09-15 19:46:52 +00:00
setSelectedDepartment(department)
setModalTitle('Departman Düzenle')
setIsFormModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const handleView = (department: HrDepartment) => {
2025-09-15 19:46:52 +00:00
setSelectedDepartment(department)
setIsViewModalOpen(true)
}
2025-09-15 09:31:47 +00:00
const handleDelete = (id: string) => {
2025-09-15 19:46:52 +00:00
if (confirm('Bu departmanı silmek istediğinizden emin misiniz?')) {
setDepartments((prev) => prev.filter((dept) => dept.id !== id))
2025-09-15 09:31:47 +00:00
}
2025-09-15 19:46:52 +00:00
}
2025-09-15 09:31:47 +00:00
const handleSave = (departmentData: Partial<HrDepartment>) => {
if (selectedDepartment) {
// Edit existing department
setDepartments((prev) =>
prev.map((dept) =>
dept.id === selectedDepartment.id
? { ...dept, ...departmentData, lastModificationTime: new Date() }
2025-09-15 19:46:52 +00:00
: dept,
),
)
2025-09-15 09:31:47 +00:00
} else {
// Add new department
const newDepartment: HrDepartment = {
id: `dept_${Date.now()}`,
...departmentData,
subDepartments: [],
creationTime: new Date(),
lastModificationTime: new Date(),
2025-09-15 19:46:52 +00:00
} as HrDepartment
setDepartments((prev) => [...prev, newDepartment])
2025-09-15 09:31:47 +00:00
}
2025-09-15 19:46:52 +00:00
setIsFormModalOpen(false)
}
2025-09-15 09:31:47 +00:00
const handleCloseFormModal = () => {
2025-09-15 19:46:52 +00:00
setIsFormModalOpen(false)
setSelectedDepartment(undefined)
}
2025-09-15 09:31:47 +00:00
const handleCloseViewModal = () => {
2025-09-15 19:46:52 +00:00
setIsViewModalOpen(false)
setSelectedDepartment(undefined)
}
2025-09-15 09:31:47 +00:00
const handleEditFromView = (department: HrDepartment) => {
2025-09-15 19:46:52 +00:00
setIsViewModalOpen(false)
handleEdit(department)
}
2025-09-15 09:31:47 +00:00
mockDepartments.forEach((dept) => {
if (dept.managerId) {
2025-09-15 19:46:52 +00:00
dept.manager = mockEmployees.find((emp) => emp.id === dept.managerId)
2025-09-15 09:31:47 +00:00
}
if (dept.costCenterId) {
2025-09-15 19:46:52 +00:00
dept.costCenter = mockCostCenters.find((cc) => cc.id === dept.costCenterId)
2025-09-15 09:31:47 +00:00
}
2025-09-15 19:46:52 +00:00
})
2025-09-15 09:31:47 +00:00
const filteredDepartments = departments.filter((department) => {
if (
searchTerm &&
!department.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!department.code.toLowerCase().includes(searchTerm.toLowerCase())
) {
2025-09-15 19:46:52 +00:00
return false
2025-09-15 09:31:47 +00:00
}
2025-09-15 19:46:52 +00:00
if (selectedParent !== 'all' && department.parentDepartmentId !== selectedParent) {
return false
2025-09-15 09:31:47 +00:00
}
2025-09-15 19:46:52 +00:00
return true
})
2025-09-15 09:31:47 +00:00
const columns: Column<HrDepartment>[] = [
{
2025-09-15 19:46:52 +00:00
key: 'code',
header: 'Departman Kodu',
2025-09-15 09:31:47 +00:00
sortable: true,
},
{
2025-09-15 19:46:52 +00:00
key: 'name',
header: 'Departman Adı',
2025-09-15 09:31:47 +00:00
sortable: true,
},
{
2025-09-15 19:46:52 +00:00
key: 'parentDepartment',
header: 'Üst Departman',
render: (department: HrDepartment) => department.parentDepartment?.name || '-',
2025-09-15 09:31:47 +00:00
},
{
2025-09-15 19:46:52 +00:00
key: 'manager',
header: 'Yönetici',
render: (department: HrDepartment) => department.manager?.fullName || '-',
2025-09-15 09:31:47 +00:00
},
{
2025-09-15 19:46:52 +00:00
key: 'employeeCount',
header: 'Personel Sayısı',
2025-09-15 09:31:47 +00:00
render: (department: HrDepartment) => (
<div className="flex items-center gap-1">
<FaUsers className="w-4 h-4 text-gray-500" />
<span>{mockEmployees.filter((a) => a.departmentId == department.id).length || 0}</span>
2025-09-15 09:31:47 +00:00
</div>
),
},
{
2025-09-15 19:46:52 +00:00
key: 'costCenter',
header: 'Maliyet Merkezi',
2025-09-15 09:31:47 +00:00
render: (department: HrDepartment) => (
<div className="flex flex-col">
2025-09-15 19:46:52 +00:00
<span className="font-medium">{department.costCenter?.code || '-'}</span>
<span className="text-xs text-gray-500">{department.costCenter?.name || '-'}</span>
2025-09-15 09:31:47 +00:00
</div>
),
},
{
2025-09-15 19:46:52 +00:00
key: 'budget',
header: 'Bütçe',
2025-09-15 09:31:47 +00:00
render: (department: HrDepartment) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-500" />
2025-09-15 19:46:52 +00:00
<span>{department.budget ? `${department.budget.toLocaleString()}` : '-'}</span>
2025-09-15 09:31:47 +00:00
</div>
),
},
{
2025-09-15 19:46:52 +00:00
key: 'status',
header: 'Durum',
2025-09-15 09:31:47 +00:00
render: (department: HrDepartment) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
2025-09-15 19:46:52 +00:00
department.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
2025-09-15 09:31:47 +00:00
}`}
>
2025-09-15 19:46:52 +00:00
{department.isActive ? 'Aktif' : 'Pasif'}
2025-09-15 09:31:47 +00:00
</span>
),
},
{
2025-09-15 19:46:52 +00:00
key: 'actions',
header: 'İşlemler',
2025-09-15 09:31:47 +00:00
render: (department: HrDepartment) => (
<div className="flex gap-2">
<button
onClick={() => handleView(department)}
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(department)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(department.id)}
className="p-1 text-red-600 hover:bg-red-50 rounded"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
),
},
2025-09-15 19:46:52 +00:00
]
2025-09-15 09:31:47 +00:00
return (
2025-09-15 19:46:52 +00:00
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">Departman Yönetimi</h2>
<p className="text-gray-600">
2025-09-15 19:46:52 +00:00
Organizasyon yapısını ve departmanları yönetin
</p>
</div>
<div className="flex items-center gap-3">
{/* 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>
2025-09-15 09:31:47 +00:00
<button
2025-09-15 19:46:52 +00:00
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"
2025-09-15 09:31:47 +00:00
>
2025-09-15 19:46:52 +00:00
<FaPlus className="w-4 h-4" />
Yeni Departman
2025-09-15 09:31:47 +00:00
</button>
</div>
</div>
2025-09-15 19:46:52 +00:00
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget
title="Toplam Departman"
value={departments.length}
color="blue"
icon="FaBuilding"
/>
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
<Widget
title="Toplam Personel"
value={mockEmployees.length}
color="green"
icon="FaUsers"
/>
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
<Widget
title="Ana Departmanlar"
value={departments.filter((d) => !d.parentDepartmentId).length}
color="purple"
icon="FaBuilding"
/>
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
<Widget
title="Toplam Bütçe"
value={`${departments
.reduce((total, dept) => total + (dept.budget || 0), 0)
.toLocaleString()}`}
color="orange"
icon="FaDollarSign"
2025-09-15 09:31:47 +00:00
/>
</div>
2025-09-15 19:46:52 +00:00
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
<div className="flex-1">
<input
type="text"
placeholder="Departman 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>
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
<select
value={selectedParent}
onChange={(e) => setSelectedParent(e.target.value)}
className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[200px]"
>
<option value="all">Tüm Departmanlar</option>
{departments
.filter((d) => !d.parentDepartmentId)
.map((dept) => (
<option key={dept.id} value={dept.id}>
{dept.name} Alt Departmanları
</option>
))}
</select>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 19:46:52 +00:00
{/* Data Display */}
{viewMode === 'list' ? (
<div className="bg-white rounded-lg shadow-sm border overflow-x-auto">
<DataTable data={filteredDepartments} columns={columns} />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{filteredDepartments.map((department) => (
<div
key={department.id}
className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow p-3"
>
{/* Card Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
<div
className={`w-12 h-12 rounded-lg flex items-center justify-center ${
department.isActive
? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-500'
}`}
>
<FaBuilding className="w-6 h-6" />
</div>
<div>
<h3 className="font-semibold text-sm text-gray-900 truncate">
{department.name}
</h3>
2025-09-15 21:02:48 +00:00
<p className="text-gray-600">{department.code}</p>
2025-09-15 19:46:52 +00:00
</div>
</div>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
2025-09-15 09:31:47 +00:00
department.isActive
2025-09-15 19:46:52 +00:00
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
2025-09-15 09:31:47 +00:00
}`}
>
2025-09-15 19:46:52 +00:00
{department.isActive ? 'Aktif' : 'Pasif'}
</span>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 19:46:52 +00:00
{/* Card Content */}
<div className="space-y-2">
{department.parentDepartment && (
<div className="flex items-center text-xs text-gray-600">
<FaBuilding className="w-4 h-4 mr-2 text-gray-400" />
<span>Üst: {department.parentDepartment.name}</span>
</div>
)}
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
{department.manager && (
<div className="flex items-center text-xs text-gray-600">
<FaUsers className="w-4 h-4 mr-2 text-gray-400" />
<span>Yönetici: {department.manager.fullName}</span>
</div>
)}
2025-09-15 09:31:47 +00:00
<div className="flex items-center text-xs text-gray-600">
2025-09-15 19:46:52 +00:00
<FaUsers className="w-4 h-4 mr-2 text-gray-400" />
<span>
{mockEmployees.filter((emp) => emp.departmentId === department.id).length}{' '}
2025-09-15 19:46:52 +00:00
Personel
</span>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 19:46:52 +00:00
{department.budget && (
<div className="flex items-center text-xs text-gray-600">
<FaDollarSign className="w-4 h-4 mr-2 text-gray-400" />
<span>{department.budget.toLocaleString()}</span>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 19:46:52 +00:00
)}
{department.costCenter && (
<div className="text-xs text-gray-600">
<div className="font-medium">{department.costCenter.code}</div>
<div className="text-xs text-gray-500">{department.costCenter.name}</div>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 19:46:52 +00:00
)}
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
{/* Card Actions */}
<div className="flex justify-end space-x-1 mt-3 pt-3 border-t border-gray-100">
<button
onClick={() => handleView(department)}
className="p-1.5 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
title="Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(department)}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(department.id)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 19:46:52 +00:00
))}
</div>
)}
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
{filteredDepartments.length === 0 && (
<div className="text-center py-12">
<FaBuilding className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Departman bulunamadı</h3>
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
</div>
2025-09-15 09:31:47 +00:00
{/* Modals */}
<DepartmentFormModal
isOpen={isFormModalOpen}
onClose={handleCloseFormModal}
onSave={handleSave}
department={selectedDepartment}
title={modalTitle}
/>
<DepartmentViewModal
isOpen={isViewModalOpen}
onClose={handleCloseViewModal}
department={selectedDepartment || null}
onEdit={handleEditFromView}
/>
2025-09-15 19:46:52 +00:00
</Container>
)
}
2025-09-15 09:31:47 +00:00
2025-09-15 19:46:52 +00:00
export default DepartmentManagement