1345 lines
51 KiB
TypeScript
1345 lines
51 KiB
TypeScript
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
|