erp-platform/ui/src/views/hr/components/PayrollManagement.tsx
2025-09-17 12:46:58 +03:00

1345 lines
51 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 {
FaCalculator,
FaFileAlt,
FaDownload,
FaEye,
FaEdit,
FaPlus,
FaTimes,
FaSave,
FaCheck,
FaBan,
FaUsers,
} from 'react-icons/fa'
import { HrPayroll, PayrollStatusEnum } from '../../../types/hr'
import DataTable, { Column } from '../../../components/common/DataTable'
import { mockEmployees } from '../../../mocks/mockEmployees'
import { mockPayrolls } from '../../../mocks/mockPayrolls'
import { mockDepartments } from '../../../mocks/mockDepartments'
import Widget from '../../../components/common/Widget'
import { getPayrollStatusColor, getPayrollStatusText } from '../../../utils/erp'
import { Container } from '@/components/shared'
// Mock data - replace with actual data fetching
const PayrollManagement: React.FC = () => {
const [payrolls, setPayrolls] = useState<HrPayroll[]>(mockPayrolls)
const [selectedStatus, setSelectedStatus] = useState<string>('all')
const [selectedPeriod, setSelectedPeriod] = useState<string>('all')
const [selectedDepartment, setSelectedDepartment] = useState<string>('all')
const [showModal, setShowModal] = useState<boolean>(false)
const [showBulkModal, setShowBulkModal] = useState<boolean>(false)
const [modalType, setModalType] = useState<'add' | 'edit' | 'view'>('add')
const [selectedPayroll, setSelectedPayroll] = useState<HrPayroll | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [formData, setFormData] = useState<Partial<HrPayroll>>({
period: '',
baseSalary: 0,
overtime: 0,
bonus: 0,
allowances: [],
deductions: [],
status: PayrollStatusEnum.Draft,
})
const handleAdd = () => {
setModalType('add')
setFormData({
period: new Date().getFullYear() + '-' + String(new Date().getMonth() + 1).padStart(2, '0'),
baseSalary: 0,
overtime: 0,
bonus: 0,
allowances: [],
deductions: [],
status: PayrollStatusEnum.Draft,
})
setSelectedPayroll(null)
setShowModal(true)
}
const handleEdit = (payroll: HrPayroll) => {
setModalType('edit')
setFormData(payroll)
setSelectedPayroll(payroll)
setShowModal(true)
}
const handleView = (payroll: HrPayroll) => {
setModalType('view')
setFormData(payroll)
setSelectedPayroll(payroll)
setShowModal(true)
}
const handleCalculate = (payroll: HrPayroll) => {
// Calculate gross and net salary
const totalAllowances = payroll.allowances.reduce(
(total, allowance) => total + allowance.amount,
0,
)
const totalDeductions = payroll.deductions.reduce(
(total, deduction) => total + deduction.amount,
0,
)
const grossSalary = payroll.baseSalary + totalAllowances + payroll.overtime + payroll.bonus
const tax = grossSalary * 0.15 // 15% tax rate (simplified)
const socialSecurity = grossSalary * 0.14 // 14% social security (simplified)
const netSalary = grossSalary - tax - socialSecurity - totalDeductions
const updatedPayroll: HrPayroll = {
...payroll,
grossSalary,
tax,
socialSecurity,
netSalary,
status: PayrollStatusEnum.Calculated,
lastModificationTime: new Date(),
}
setPayrolls((prevPayrolls) =>
prevPayrolls.map((p) => (p.id === payroll.id ? updatedPayroll : p)),
)
alert(
`Bordro hesaplandı:\nBrüt Maaş: ₺${grossSalary.toLocaleString()}\nNet Maaş: ₺${netSalary.toLocaleString()}`,
)
}
const handleApprove = (payroll: HrPayroll) => {
const updatedPayroll: HrPayroll = {
...payroll,
status: PayrollStatusEnum.Approved,
lastModificationTime: new Date(),
}
setPayrolls((prevPayrolls) =>
prevPayrolls.map((p) => (p.id === payroll.id ? updatedPayroll : p)),
)
alert(`Bordro onaylandı: ${payroll.employee?.fullName}`)
}
const handleReject = (payroll: HrPayroll) => {
const updatedPayroll: HrPayroll = {
...payroll,
status: PayrollStatusEnum.Cancelled,
lastModificationTime: new Date(),
}
setPayrolls((prevPayrolls) =>
prevPayrolls.map((p) => (p.id === payroll.id ? updatedPayroll : p)),
)
alert(`Bordro reddedildi: ${payroll.employee?.fullName}`)
}
const handleBulkEntry = () => {
setShowBulkModal(true)
}
const handleExport = (payroll: HrPayroll) => {
// Create a simple CSV export
const csvContent = [
['Personel Kodu', payroll.employee?.code || ''],
['Personel Adı', payroll.employee?.fullName || ''],
['Dönem', payroll.period],
['Temel Maaş', `${payroll.baseSalary.toLocaleString()}`],
['Mesai Ücreti', `${payroll.overtime.toLocaleString()}`],
['Prim', `${payroll.bonus.toLocaleString()}`],
['Brüt Maaş', `${payroll.grossSalary.toLocaleString()}`],
['Vergi', `${payroll.tax.toLocaleString()}`],
['SGK', `${payroll.socialSecurity.toLocaleString()}`],
['Net Maaş', `${payroll.netSalary.toLocaleString()}`],
['Durum', getPayrollStatusText(payroll.status)],
]
.map((row) => row.join(','))
.join('\n')
const blob = new Blob(['\uFEFF' + csvContent], {
type: 'text/csv;charset=utf-8;',
})
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `bordro_${payroll.employee?.code}_${payroll.period}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const handleSubmit = () => {
if (modalType === 'add') {
const newPayroll: HrPayroll = {
id: Date.now().toString(),
employeeId: formData.employeeId || '',
employee: mockEmployees.find((emp) => emp.id === formData.employeeId),
period: formData.period || '',
baseSalary: formData.baseSalary || 0,
allowances: formData.allowances || [],
deductions: formData.deductions || [],
overtime: formData.overtime || 0,
bonus: formData.bonus || 0,
grossSalary: 0,
netSalary: 0,
tax: 0,
socialSecurity: 0,
status: PayrollStatusEnum.Draft,
creationTime: new Date(),
lastModificationTime: new Date(),
}
setPayrolls((prevPayrolls) => [...prevPayrolls, newPayroll])
} else if (modalType === 'edit' && selectedPayroll) {
const updatedPayroll: HrPayroll = {
...selectedPayroll,
...formData,
lastModificationTime: new Date(),
}
setPayrolls((prevPayrolls) =>
prevPayrolls.map((p) => (p.id === selectedPayroll.id ? updatedPayroll : p)),
)
}
setShowModal(false)
setSelectedPayroll(null)
setFormData({
period: '',
baseSalary: 0,
overtime: 0,
bonus: 0,
allowances: [],
deductions: [],
status: PayrollStatusEnum.Draft,
})
}
const handleCancel = () => {
setShowModal(false)
setSelectedPayroll(null)
setFormData({
period: '',
baseSalary: 0,
overtime: 0,
bonus: 0,
allowances: [],
deductions: [],
status: PayrollStatusEnum.Draft,
})
}
// Calculate gross and net salary helper function
const calculateSalaries = (data: Partial<HrPayroll>) => {
const totalAllowances = (data.allowances || []).reduce(
(total, allowance) => total + allowance.amount,
0,
)
const totalDeductions = (data.deductions || []).reduce(
(total, deduction) => total + deduction.amount,
0,
)
const grossSalary =
(data.baseSalary || 0) + totalAllowances + (data.overtime || 0) + (data.bonus || 0)
const tax = grossSalary * 0.15 // 15% tax rate (simplified)
const socialSecurity = grossSalary * 0.14 // 14% social security (simplified)
const netSalary = grossSalary - tax - socialSecurity - totalDeductions
return {
grossSalary,
tax,
socialSecurity,
netSalary,
}
}
// Real-time calculation for form data
const calculatedValues = calculateSalaries(formData)
const filteredPayrolls = payrolls.filter((payroll) => {
if (selectedStatus !== 'all' && payroll.status !== selectedStatus) {
return false
}
if (selectedPeriod !== 'all' && payroll.period !== selectedPeriod) {
return false
}
if (selectedDepartment !== 'all' && payroll.employee?.department?.name !== selectedDepartment) {
return false
}
if (searchTerm.trim() !== '') {
if (payroll.employeeId !== searchTerm) {
return false
}
}
return true
})
const columns: Column<HrPayroll>[] = [
{
key: 'employee',
header: 'Personel',
render: (payroll: HrPayroll) => (
<div>
<div className="font-medium text-gray-900">{payroll.employee?.fullName}</div>
<div className="text-sm text-gray-500">{payroll.employee?.code}</div>
</div>
),
},
{
key: 'period',
header: 'Dönem',
render: (payroll: HrPayroll) => payroll.period,
},
{
key: 'baseSalary',
header: 'Temel Maaş',
render: (payroll: HrPayroll) => `${payroll.baseSalary.toLocaleString()}`,
},
{
key: 'allowances',
header: 'Ödemeler',
render: (payroll: HrPayroll) => {
const totalAllowances = payroll.allowances.reduce(
(total, allowance) => total + allowance.amount,
0,
)
return `${totalAllowances.toLocaleString()}`
},
},
{
key: 'deductions',
header: 'Kesintiler',
render: (payroll: HrPayroll) => {
const totalDeductions = payroll.deductions.reduce(
(total, deduction) => total + deduction.amount,
0,
)
return `${totalDeductions.toLocaleString()}`
},
},
{
key: 'grossSalary',
header: 'Brüt Maaş',
render: (payroll: HrPayroll) => (
<div className="font-medium text-gray-900">{payroll.grossSalary.toLocaleString()}</div>
),
},
{
key: 'netSalary',
header: 'Net Maaş',
render: (payroll: HrPayroll) => (
<div className="font-bold text-green-600">{payroll.netSalary.toLocaleString()}</div>
),
},
{
key: 'status',
header: 'Durum',
render: (payroll: HrPayroll) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPayrollStatusColor(
payroll.status,
)}`}
>
{getPayrollStatusText(payroll.status)}
</span>
),
},
{
key: 'actions',
header: 'İşlemler',
render: (payroll: HrPayroll) => (
<div className="flex gap-1">
<button
onClick={() => handleView(payroll)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
{payroll.status === PayrollStatusEnum.Draft && (
<button
onClick={() => handleCalculate(payroll)}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Hesapla"
>
<FaCalculator className="w-4 h-4" />
</button>
)}
{payroll.status === PayrollStatusEnum.Calculated && (
<>
<button
onClick={() => handleApprove(payroll)}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Onayla"
>
<FaCheck className="w-4 h-4" />
</button>
<button
onClick={() => handleReject(payroll)}
className="p-1 text-red-600 hover:bg-red-50 rounded"
title="Reddet"
>
<FaBan className="w-4 h-4" />
</button>
</>
)}
<button
onClick={() => handleEdit(payroll)}
className="p-1 text-gray-600 hover:bg-gray-50 rounded"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
{(payroll.status === PayrollStatusEnum.Approved ||
payroll.status === PayrollStatusEnum.Paid) && (
<button
onClick={() => handleExport(payroll)}
className="p-1 text-purple-600 hover:bg-purple-50 rounded"
title="Dışa Aktar"
>
<FaDownload className="w-4 h-4" />
</button>
)}
</div>
),
},
]
// Calculate statistics
const stats = {
total: payrolls.length,
totalGross: payrolls.reduce((total, p) => total + p.grossSalary, 0),
totalNet: payrolls.reduce((total, p) => total + p.netSalary, 0),
totalTax: payrolls.reduce((total, p) => total + p.tax, 0),
pending: payrolls.filter(
(p) => p.status === PayrollStatusEnum.Draft || p.status === PayrollStatusEnum.Calculated,
).length,
}
// Get unique periods for filter
const periods = [...new Set(payrolls.map((p) => p.period))].sort().reverse()
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">Maaş, Prim, Bordro Yönetimi</h2>
<p className="text-gray-600">Personel ödemelerini hesaplayın ve yönetin</p>
</div>
<div className="flex gap-2">
<button
onClick={handleBulkEntry}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
>
<FaUsers className="w-4 h-4" />
Toplu Bordro Girişi
</button>
<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 Bordro
</button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
<Widget title="Toplam Bordro" value={stats.total} color="blue" icon="FaFileAlt" />
<Widget
title="Toplam Brüt"
value={`${stats.totalGross.toLocaleString()}`}
color="green"
icon="FaDollarSign"
/>
<Widget
title="Toplam Net"
value={`${stats.totalNet.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Toplam Vergi"
value={`${stats.totalTax.toLocaleString()}`}
color="red"
icon="FaCalculator"
/>
<Widget title="Beklemede" value={stats.pending} color="yellow" icon="FaFileAlt" />
</div>
{/* Filters */}
<div className="flex gap-4 items-center">
<select
value={selectedDepartment}
onChange={(e) => setSelectedDepartment(e.target.value)}
className="px-3 py-1.5 border border-gray-300 text-sm 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.name}>
{dept.name}
</option>
))}
</select>
<select
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-1.5 border border-gray-300 text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Personel seçiniz...</option>
{mockEmployees.map((employee) => (
<option key={employee.id} value={employee.id}>
{employee.fullName} ({employee.code})
</option>
))}
</select>
<select
value={selectedStatus}
onChange={(e) => setSelectedStatus(e.target.value)}
className="px-3 py-1.5 border border-gray-300 text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Durumlar</option>
{Object.values(PayrollStatusEnum).map((status) => (
<option key={status} value={status}>
{getPayrollStatusText(status)}
</option>
))}
</select>
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className="px-3 py-1.5 border border-gray-300 text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Dönemler</option>
{periods.map((period) => (
<option key={period} value={period}>
{period}
</option>
))}
</select>
</div>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredPayrolls} columns={columns} />
</div>
{filteredPayrolls.length === 0 && (
<div className="text-center py-12">
<FaFileAlt className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Bordro bulunamadı</h3>
<p className="text-gray-500">Seçilen kriterlere uygun bordro bulunmamaktadır.</p>
</div>
)}
</div>
{/* Modal */}
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">
{modalType === 'add' && 'Yeni Bordro Ekle'}
{modalType === 'edit' && 'Bordro Düzenle'}
{modalType === 'view' && 'Bordro Detayları'}
</h3>
<button onClick={handleCancel} className="text-gray-400 hover:text-gray-600">
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
{/* Employee Selection */}
{modalType === 'add' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Personel</label>
<select
value={formData.employeeId || ''}
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Personel Seçiniz</option>
{mockEmployees.map((emp) => (
<option key={emp.id} value={emp.id}>
{emp.code} - {emp.fullName}
</option>
))}
</select>
</div>
)}
{/* Employee Info (for edit/view) */}
{(modalType === 'edit' || modalType === 'view') && formData.employee && (
<div className="bg-gray-50 p-3 rounded-md">
<div className="text-sm text-gray-600">Personel</div>
<div className="font-medium">
{formData.employee.code} - {formData.employee.fullName}
</div>
</div>
)}
{/* Period */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dönem</label>
<input
type="month"
value={formData.period}
onChange={(e) => setFormData({ ...formData, period: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={modalType === 'view'}
/>
</div>
{/* Base Salary */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Temel Maaş ()
</label>
<input
type="number"
value={formData.baseSalary}
onChange={(e) =>
setFormData({
...formData,
baseSalary: Number(e.target.value),
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={modalType === 'view'}
/>
</div>
{/* Overtime */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Mesai Ücreti ()
</label>
<input
type="number"
value={formData.overtime}
onChange={(e) =>
setFormData({
...formData,
overtime: Number(e.target.value),
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={modalType === 'view'}
/>
</div>
{/* Bonus */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Prim ()</label>
<input
type="number"
value={formData.bonus}
onChange={(e) => setFormData({ ...formData, bonus: Number(e.target.value) })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={modalType === 'view'}
/>
</div>
{/* Allowances */}
{modalType !== 'view' && (
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">Ödemeler</label>
<button
type="button"
onClick={() => {
const newAllowances = [
...(formData.allowances || []),
{
id: Date.now().toString(),
name: '',
amount: 0,
taxable: true,
},
]
setFormData({ ...formData, allowances: newAllowances })
}}
className="text-blue-600 hover:text-blue-700 text-sm"
>
+ Ödeme Ekle
</button>
</div>
{formData.allowances?.map((allowance, index) => (
<div key={allowance.id} className="flex gap-2 mb-2">
<input
type="text"
placeholder="Ödeme adı"
value={allowance.name}
onChange={(e) => {
const updatedAllowances = [...(formData.allowances || [])]
updatedAllowances[index] = {
...allowance,
name: e.target.value,
}
setFormData({
...formData,
allowances: updatedAllowances,
})
}}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="Tutar"
value={allowance.amount}
onChange={(e) => {
const updatedAllowances = [...(formData.allowances || [])]
updatedAllowances[index] = {
...allowance,
amount: Number(e.target.value),
}
setFormData({
...formData,
allowances: updatedAllowances,
})
}}
className="w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => {
const updatedAllowances =
formData.allowances?.filter((_, i) => i !== index) || []
setFormData({
...formData,
allowances: updatedAllowances,
})
}}
className="px-2 py-2 text-red-600 hover:text-red-700"
>
<FaTimes className="w-4 h-4" />
</button>
</div>
))}
{(!formData.allowances || formData.allowances.length === 0) && (
<p className="text-sm text-gray-500">Henüz ödeme eklenmedi</p>
)}
</div>
)}
{/* Deductions */}
{modalType !== 'view' && (
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700">Kesintiler</label>
<button
type="button"
onClick={() => {
const newDeductions = [
...(formData.deductions || []),
{
id: Date.now().toString(),
name: '',
amount: 0,
mandatory: false,
},
]
setFormData({ ...formData, deductions: newDeductions })
}}
className="text-blue-600 hover:text-blue-700 text-sm"
>
+ Kesinti Ekle
</button>
</div>
{formData.deductions?.map((deduction, index) => (
<div key={deduction.id} className="flex gap-2 mb-2">
<input
type="text"
placeholder="Kesinti adı"
value={deduction.name}
onChange={(e) => {
const updatedDeductions = [...(formData.deductions || [])]
updatedDeductions[index] = {
...deduction,
name: e.target.value,
}
setFormData({
...formData,
deductions: updatedDeductions,
})
}}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="number"
placeholder="Tutar"
value={deduction.amount}
onChange={(e) => {
const updatedDeductions = [...(formData.deductions || [])]
updatedDeductions[index] = {
...deduction,
amount: Number(e.target.value),
}
setFormData({
...formData,
deductions: updatedDeductions,
})
}}
className="w-24 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="button"
onClick={() => {
const updatedDeductions =
formData.deductions?.filter((_, i) => i !== index) || []
setFormData({
...formData,
deductions: updatedDeductions,
})
}}
className="px-2 py-2 text-red-600 hover:text-red-700"
>
<FaTimes className="w-4 h-4" />
</button>
</div>
))}
{(!formData.deductions || formData.deductions.length === 0) && (
<p className="text-sm text-gray-500">Henüz kesinti eklenmedi</p>
)}
</div>
)}
{/* Real-time calculation preview for add/edit modes */}
{modalType !== 'view' && (
<div className="bg-blue-50 p-4 rounded-lg">
<h5 className="font-medium text-gray-900 mb-3">Hesaplama Önizlemesi</h5>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-gray-600">Brüt Maaş</div>
<div className="font-medium text-lg text-gray-900">
{calculatedValues.grossSalary.toLocaleString()}
</div>
<div className="text-xs text-gray-500">
Temel: {(formData.baseSalary || 0).toLocaleString()} + Ödemeler:
{(formData.allowances || [])
.reduce((total, allowance) => total + allowance.amount, 0)
.toLocaleString()}{' '}
+ Mesai: {(formData.overtime || 0).toLocaleString()} + Prim:
{(formData.bonus || 0).toLocaleString()}
</div>
</div>
<div>
<div className="text-sm text-gray-600">Net Maaş</div>
<div className="font-bold text-lg text-green-600">
{calculatedValues.netSalary.toLocaleString()}
</div>
<div className="text-xs text-gray-500">
Brüt - Vergi: {calculatedValues.tax.toLocaleString()} - SGK:
{calculatedValues.socialSecurity.toLocaleString()} - Kesintiler:
{(formData.deductions || [])
.reduce((total, deduction) => total + deduction.amount, 0)
.toLocaleString()}
</div>
</div>
</div>
</div>
)}
{/* View mode - show allowances and deductions */}
{modalType === 'view' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Ödemeler</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md">
{formData.allowances && formData.allowances.length > 0 ? (
<div className="space-y-1">
{formData.allowances.map((allowance) => (
<div key={allowance.id} className="flex justify-between text-sm">
<span>{allowance.name}</span>
<span>{allowance.amount.toLocaleString()}</span>
</div>
))}
<div className="border-t pt-1 font-medium flex justify-between">
<span>Toplam:</span>
<span>
{formData.allowances
.reduce((total, allowance) => total + allowance.amount, 0)
.toLocaleString()}
</span>
</div>
</div>
) : (
<span className="text-gray-500">Ödeme yok</span>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kesintiler
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md">
{formData.deductions && formData.deductions.length > 0 ? (
<div className="space-y-1">
{formData.deductions.map((deduction) => (
<div key={deduction.id} className="flex justify-between text-sm">
<span>{deduction.name}</span>
<span>{deduction.amount.toLocaleString()}</span>
</div>
))}
<div className="border-t pt-1 font-medium flex justify-between">
<span>Toplam:</span>
<span>
{formData.deductions
.reduce((total, deduction) => total + deduction.amount, 0)
.toLocaleString()}
</span>
</div>
</div>
) : (
<span className="text-gray-500">Kesinti yok</span>
)}
</div>
</div>
</>
)}
{/* Calculated fields (show only in view/edit mode for existing payrolls) */}
{modalType === 'view' && (
<>
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Durum</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md">
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getPayrollStatusColor(
formData.status || PayrollStatusEnum.Draft,
)}`}
>
{getPayrollStatusText(formData.status || PayrollStatusEnum.Draft)}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Brüt Maaş ()
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md">
{formData.grossSalary?.toLocaleString()}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Net Maaş ()
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md font-bold text-green-600">
{formData.netSalary?.toLocaleString()}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Vergi ()
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md">
{formData.tax?.toLocaleString()}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
SGK ()
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md">
{formData.socialSecurity?.toLocaleString()}
</div>
</div>
</div>
</>
)}
</div>
{/* Modal Actions */}
{modalType !== 'view' && (
<div className="flex justify-end gap-2 mt-6">
<button
onClick={handleCancel}
className="px-4 py-2 text-gray-600 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors"
>
İptal
</button>
<button
onClick={handleSubmit}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<FaSave className="w-4 h-4" />
{modalType === 'add' ? 'Ekle' : 'Güncelle'}
</button>
</div>
)}
{modalType === 'view' && (
<div className="flex justify-end mt-6">
<button
onClick={handleCancel}
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
>
Kapat
</button>
</div>
)}
</div>
</div>
)}
{/* Bulk Entry Modal */}
{showBulkModal && (
<BulkPayrollEntry
onClose={() => setShowBulkModal(false)}
onSubmit={(payrolls) => {
setPayrolls((prev) => [...prev, ...payrolls])
setShowBulkModal(false)
}}
/>
)}
</Container>
)
}
// Bulk Payroll Entry Component
interface BulkPayrollEntryProps {
onClose: () => void
onSubmit: (payrolls: HrPayroll[]) => void
}
const BulkPayrollEntry: React.FC<BulkPayrollEntryProps> = ({ onClose, onSubmit }) => {
const [selectedDepartment, setSelectedDepartment] = useState<string>('')
const [selectedEmployees, setSelectedEmployees] = useState<string[]>([])
const [period, setPeriod] = useState<string>(
new Date().getFullYear() + '-' + String(new Date().getMonth() + 1).padStart(2, '0'),
)
const [bulkData, setBulkData] = useState({
baseSalaryIncrease: 0,
overtimeHours: 0,
overtimeRate: 50,
bonusAmount: 0,
massBonus: 0,
})
// Get unique departments from mockDepartments and employees
const departments = [
...new Set([
...mockDepartments.map((dept) => dept.name),
...mockEmployees.map((emp) => emp.department?.name).filter(Boolean),
]),
].sort()
// Get employees by department
const filteredEmployees = selectedDepartment
? mockEmployees.filter((emp) => emp.department?.name === selectedDepartment)
: []
// Get selected employees data
const selectedEmployeesData = filteredEmployees.filter((emp) =>
selectedEmployees.includes(emp.id),
)
// Handle department change - reset selected employees and auto-select all
const handleDepartmentChange = (departmentName: string) => {
setSelectedDepartment(departmentName)
// Auto-select all employees when department is selected
if (departmentName) {
const employeesInDept = mockEmployees.filter((emp) => emp.department?.name === departmentName)
setSelectedEmployees(employeesInDept.map((emp) => emp.id))
} else {
setSelectedEmployees([])
}
}
// Handle select all employees
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedEmployees(filteredEmployees.map((emp) => emp.id))
} else {
setSelectedEmployees([])
}
}
// Handle individual employee selection
const handleEmployeeSelect = (employeeId: string, checked: boolean) => {
if (checked) {
setSelectedEmployees((prev) => [...prev, employeeId])
} else {
setSelectedEmployees((prev) => prev.filter((id) => id !== employeeId))
}
}
const handleBulkSubmit = () => {
if (!selectedDepartment || selectedEmployeesData.length === 0) {
alert('Lütfen bir departman ve en az bir personel seçin')
return
}
const newPayrolls: HrPayroll[] = selectedEmployeesData.map((employee) => {
const baseSalary = employee.baseSalary + bulkData.baseSalaryIncrease
const overtime = bulkData.overtimeHours * bulkData.overtimeRate
const bonus = bulkData.bonusAmount + bulkData.massBonus
return {
id: `bulk_${Date.now()}_${employee.id}`,
employeeId: employee.id,
employee: employee,
period: period,
baseSalary: baseSalary,
allowances: [],
deductions: [],
overtime: overtime,
bonus: bonus,
grossSalary: 0,
netSalary: 0,
tax: 0,
socialSecurity: 0,
status: PayrollStatusEnum.Draft,
creationTime: new Date(),
lastModificationTime: new Date(),
}
})
onSubmit(newPayrolls)
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Toplu Bordro Girişi</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="space-y-6">
{/* Department and Period */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Departman</label>
<select
value={selectedDepartment}
onChange={(e) => handleDepartmentChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Departman Seçiniz</option>
{departments.map((dept) => (
<option key={dept} value={dept}>
{dept}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dönem</label>
<input
type="month"
value={period}
onChange={(e) => setPeriod(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* Bulk Settings */}
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="font-medium text-gray-900 mb-3">Toplu Ayarlar</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Maaş Artışı ()
</label>
<input
type="number"
value={bulkData.baseSalaryIncrease}
onChange={(e) =>
setBulkData({
...bulkData,
baseSalaryIncrease: Number(e.target.value),
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Mesai Saati</label>
<input
type="number"
value={bulkData.overtimeHours}
onChange={(e) =>
setBulkData({
...bulkData,
overtimeHours: Number(e.target.value),
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Mesai Saatlik Ücreti ()
</label>
<input
type="number"
value={bulkData.overtimeRate}
onChange={(e) =>
setBulkData({
...bulkData,
overtimeRate: Number(e.target.value),
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Departman Primi ()
</label>
<input
type="number"
value={bulkData.massBonus}
onChange={(e) =>
setBulkData({
...bulkData,
massBonus: Number(e.target.value),
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Employee Selection */}
{selectedDepartment && filteredEmployees.length > 0 && (
<div>
<div className="flex items-center justify-between mb-3">
<h4 className="font-medium text-gray-900">
Personel Seçimi ({filteredEmployees.length} kişi)
</h4>
<label className="flex items-center">
<input
type="checkbox"
checked={
selectedEmployees.length === filteredEmployees.length &&
filteredEmployees.length > 0
}
onChange={(e) => handleSelectAll(e.target.checked)}
className="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<span className="text-sm text-gray-700">Tümünü Seç</span>
</label>
</div>
<div className="max-h-60 overflow-y-auto border border-gray-300 rounded-md">
<table className="min-w-full">
<thead className="bg-gray-50 sticky top-0">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase w-12">
Seç
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Personel Kodu
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Ad Soyad
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Mevcut Maaş
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
Yeni Maaş
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredEmployees.map((employee) => (
<tr
key={employee.id}
className={selectedEmployees.includes(employee.id) ? 'bg-blue-50' : ''}
>
<td className="px-4 py-2">
<input
type="checkbox"
checked={selectedEmployees.includes(employee.id)}
onChange={(e) => handleEmployeeSelect(employee.id, e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
</td>
<td className="px-4 py-2 text-sm text-gray-900">{employee.code}</td>
<td className="px-4 py-2 text-sm text-gray-900">{employee.fullName}</td>
<td className="px-4 py-2 text-sm text-gray-900">
{employee.baseSalary.toLocaleString()}
</td>
<td className="px-4 py-2 text-sm font-medium text-green-600">
{(employee.baseSalary + bulkData.baseSalaryIncrease).toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
{selectedEmployees.length > 0 && (
<div className="mt-3 p-3 bg-blue-50 rounded-md">
<p className="text-sm text-blue-800">
<strong>{selectedEmployees.length}</strong> personel seçildi. Toplam mevcut
maaş:{' '}
<strong>
{selectedEmployeesData
.reduce((total, emp) => total + emp.baseSalary, 0)
.toLocaleString()}
</strong>
</p>
</div>
)}
</div>
)}
</div>
{/* Modal Actions */}
<div className="flex justify-end gap-2 mt-6">
<button
onClick={onClose}
className="px-4 py-2 text-gray-600 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors"
>
İptal
</button>
<button
onClick={handleBulkSubmit}
disabled={!selectedDepartment || selectedEmployeesData.length === 0}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
>
<FaSave className="w-4 h-4" />
Toplu Bordro Oluştur ({selectedEmployeesData.length} kişi)
</button>
</div>
</div>
</div>
)
}
export default PayrollManagement