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

461 lines
16 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 {
FaPlus,
FaEdit,
FaTrash,
FaBuilding,
FaUsers,
FaDollarSign,
FaEye,
FaList,
FaTh,
} from 'react-icons/fa'
import { DepartmentDto } 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'
const DepartmentManagement: React.FC = () => {
const [departments, setDepartments] = useState<DepartmentDto[]>(mockDepartments)
const [searchTerm, setSearchTerm] = useState('')
const [selectedParent, setSelectedParent] = useState<string>('all')
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
// Modal states
const [isFormModalOpen, setIsFormModalOpen] = useState(false)
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
const [selectedDepartment, setSelectedDepartment] = useState<DepartmentDto | undefined>()
const [modalTitle, setModalTitle] = useState('')
const handleAdd = () => {
setSelectedDepartment(undefined)
setModalTitle('Yeni Departman')
setIsFormModalOpen(true)
}
const handleEdit = (department: DepartmentDto) => {
setSelectedDepartment(department)
setModalTitle('Departman Düzenle')
setIsFormModalOpen(true)
}
const handleView = (department: DepartmentDto) => {
setSelectedDepartment(department)
setIsViewModalOpen(true)
}
const handleDelete = (id: string) => {
if (confirm('Bu departmanı silmek istediğinizden emin misiniz?')) {
setDepartments((prev) => prev.filter((dept) => dept.id !== id))
}
}
const handleSave = (departmentData: Partial<DepartmentDto>) => {
if (selectedDepartment) {
// Edit existing department
setDepartments((prev) =>
prev.map((dept) =>
dept.id === selectedDepartment.id
? { ...dept, ...departmentData, lastModificationTime: new Date() }
: dept,
),
)
} else {
// Add new department
const newDepartment: DepartmentDto = {
id: `dept_${Date.now()}`,
...departmentData,
subDepartments: [],
creationTime: new Date(),
lastModificationTime: new Date(),
} as DepartmentDto
setDepartments((prev) => [...prev, newDepartment])
}
setIsFormModalOpen(false)
}
const handleCloseFormModal = () => {
setIsFormModalOpen(false)
setSelectedDepartment(undefined)
}
const handleCloseViewModal = () => {
setIsViewModalOpen(false)
setSelectedDepartment(undefined)
}
const handleEditFromView = (department: DepartmentDto) => {
setIsViewModalOpen(false)
handleEdit(department)
}
mockDepartments.forEach((dept) => {
if (dept.managerId) {
dept.manager = mockEmployees.find((emp) => emp.id === dept.managerId)
}
if (dept.costCenterId) {
dept.costCenter = mockCostCenters.find((cc) => cc.id === dept.costCenterId)
}
})
const filteredDepartments = departments.filter((department) => {
if (
searchTerm &&
!department.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
!department.code.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false
}
if (selectedParent !== 'all' && department.parentDepartmentId !== selectedParent) {
return false
}
return true
})
const columns: Column<DepartmentDto>[] = [
{
key: 'code',
header: 'Departman Kodu',
sortable: true,
},
{
key: 'name',
header: 'Departman Adı',
sortable: true,
},
{
key: 'parentDepartment',
header: 'Üst Departman',
render: (department: DepartmentDto) => department.parentDepartment?.name || '-',
},
{
key: 'manager',
header: 'Yönetici',
render: (department: DepartmentDto) => department.manager?.fullName || '-',
},
{
key: 'employeeCount',
header: 'Personel Sayısı',
render: (department: DepartmentDto) => (
<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>
</div>
),
},
{
key: 'costCenter',
header: 'Maliyet Merkezi',
render: (department: DepartmentDto) => (
<div className="flex flex-col">
<span className="font-medium">{department.costCenter?.code || '-'}</span>
<span className="text-xs text-gray-500">{department.costCenter?.name || '-'}</span>
</div>
),
},
{
key: 'budget',
header: 'Bütçe',
render: (department: DepartmentDto) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-500" />
<span>{department.budget ? `${department.budget.toLocaleString()}` : '-'}</span>
</div>
),
},
{
key: 'status',
header: 'Durum',
render: (department: DepartmentDto) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
department.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}
>
{department.isActive ? 'Aktif' : 'Pasif'}
</span>
),
},
{
key: 'actions',
header: 'İşlemler',
render: (department: DepartmentDto) => (
<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>
),
},
]
return (
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h2 className="text-2xl font-bold text-gray-900">Departman Yönetimi</h2>
<p className="text-gray-600">
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>
<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" />
Yeni Departman
</button>
</div>
</div>
{/* 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"
/>
<Widget
title="Toplam Personel"
value={mockEmployees.length}
color="green"
icon="FaUsers"
/>
<Widget
title="Ana Departmanlar"
value={departments.filter((d) => !d.parentDepartmentId).length}
color="purple"
icon="FaBuilding"
/>
<Widget
title="Toplam Bütçe"
value={`${departments
.reduce((total, dept) => total + (dept.budget || 0), 0)
.toLocaleString()}`}
color="orange"
icon="FaDollarSign"
/>
</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="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>
<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>
</div>
{/* 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>
<p className="text-gray-600">{department.code}</p>
</div>
</div>
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${
department.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{department.isActive ? 'Aktif' : 'Pasif'}
</span>
</div>
{/* 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>
)}
{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>
)}
<div className="flex items-center text-xs text-gray-600">
<FaUsers className="w-4 h-4 mr-2 text-gray-400" />
<span>
{mockEmployees.filter((emp) => emp.departmentId === department.id).length}{' '}
Personel
</span>
</div>
{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>
</div>
)}
{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>
</div>
)}
</div>
{/* 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>
</div>
))}
</div>
)}
{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>
{/* Modals */}
<DepartmentFormModal
isOpen={isFormModalOpen}
onClose={handleCloseFormModal}
onSave={handleSave}
department={selectedDepartment}
title={modalTitle}
/>
<DepartmentViewModal
isOpen={isViewModalOpen}
onClose={handleCloseViewModal}
department={selectedDepartment || null}
onEdit={handleEditFromView}
/>
</Container>
)
}
export default DepartmentManagement