2025-09-15 19:46:52 +00:00
|
|
|
|
import React, { useState } from 'react'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
import {
|
|
|
|
|
|
FaCalculator,
|
|
|
|
|
|
FaFileAlt,
|
|
|
|
|
|
FaDownload,
|
|
|
|
|
|
FaEye,
|
|
|
|
|
|
FaEdit,
|
|
|
|
|
|
FaPlus,
|
|
|
|
|
|
FaTimes,
|
|
|
|
|
|
FaSave,
|
|
|
|
|
|
FaCheck,
|
|
|
|
|
|
FaBan,
|
|
|
|
|
|
FaUsers,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
} 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'
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Mock data - replace with actual data fetching
|
|
|
|
|
|
|
|
|
|
|
|
const PayrollManagement: React.FC = () => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
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('')
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const [formData, setFormData] = useState<Partial<HrPayroll>>({
|
2025-09-15 19:46:52 +00:00
|
|
|
|
period: '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
baseSalary: 0,
|
|
|
|
|
|
overtime: 0,
|
|
|
|
|
|
bonus: 0,
|
|
|
|
|
|
allowances: [],
|
|
|
|
|
|
deductions: [],
|
|
|
|
|
|
status: PayrollStatusEnum.Draft,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setModalType('add')
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
2025-09-15 19:46:52 +00:00
|
|
|
|
period: new Date().getFullYear() + '-' + String(new Date().getMonth() + 1).padStart(2, '0'),
|
2025-09-15 09:31:47 +00:00
|
|
|
|
baseSalary: 0,
|
|
|
|
|
|
overtime: 0,
|
|
|
|
|
|
bonus: 0,
|
|
|
|
|
|
allowances: [],
|
|
|
|
|
|
deductions: [],
|
|
|
|
|
|
status: PayrollStatusEnum.Draft,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
|
|
|
|
|
setSelectedPayroll(null)
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleEdit = (payroll: HrPayroll) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setModalType('edit')
|
|
|
|
|
|
setFormData(payroll)
|
|
|
|
|
|
setSelectedPayroll(payroll)
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleView = (payroll: HrPayroll) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setModalType('view')
|
|
|
|
|
|
setFormData(payroll)
|
|
|
|
|
|
setSelectedPayroll(payroll)
|
|
|
|
|
|
setShowModal(true)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleCalculate = (payroll: HrPayroll) => {
|
|
|
|
|
|
// Calculate gross and net salary
|
|
|
|
|
|
const totalAllowances = payroll.allowances.reduce(
|
|
|
|
|
|
(total, allowance) => total + allowance.amount,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
0,
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const totalDeductions = payroll.deductions.reduce(
|
|
|
|
|
|
(total, deduction) => total + deduction.amount,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
0,
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
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
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const updatedPayroll: HrPayroll = {
|
|
|
|
|
|
...payroll,
|
|
|
|
|
|
grossSalary,
|
|
|
|
|
|
tax,
|
|
|
|
|
|
socialSecurity,
|
|
|
|
|
|
netSalary,
|
|
|
|
|
|
status: PayrollStatusEnum.Calculated,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
setPayrolls((prevPayrolls) =>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
prevPayrolls.map((p) => (p.id === payroll.id ? updatedPayroll : p)),
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
alert(
|
2025-09-15 19:46:52 +00:00
|
|
|
|
`Bordro hesaplandı:\nBrüt Maaş: ₺${grossSalary.toLocaleString()}\nNet Maaş: ₺${netSalary.toLocaleString()}`,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleApprove = (payroll: HrPayroll) => {
|
|
|
|
|
|
const updatedPayroll: HrPayroll = {
|
|
|
|
|
|
...payroll,
|
|
|
|
|
|
status: PayrollStatusEnum.Approved,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
setPayrolls((prevPayrolls) =>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
prevPayrolls.map((p) => (p.id === payroll.id ? updatedPayroll : p)),
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
alert(`Bordro onaylandı: ${payroll.employee?.fullName}`)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleReject = (payroll: HrPayroll) => {
|
|
|
|
|
|
const updatedPayroll: HrPayroll = {
|
|
|
|
|
|
...payroll,
|
|
|
|
|
|
status: PayrollStatusEnum.Cancelled,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
setPayrolls((prevPayrolls) =>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
prevPayrolls.map((p) => (p.id === payroll.id ? updatedPayroll : p)),
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
alert(`Bordro reddedildi: ${payroll.employee?.fullName}`)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleBulkEntry = () => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setShowBulkModal(true)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleExport = (payroll: HrPayroll) => {
|
|
|
|
|
|
// Create a simple CSV export
|
|
|
|
|
|
const csvContent = [
|
2025-09-15 19:46:52 +00:00
|
|
|
|
['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)],
|
2025-09-15 09:31:47 +00:00
|
|
|
|
]
|
2025-09-15 19:46:52 +00:00
|
|
|
|
.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)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleSubmit = () => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
if (modalType === 'add') {
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const newPayroll: HrPayroll = {
|
|
|
|
|
|
id: Date.now().toString(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
employeeId: formData.employeeId || '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
employee: mockEmployees.find((emp) => emp.id === formData.employeeId),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
period: formData.period || '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setPayrolls((prevPayrolls) => [...prevPayrolls, newPayroll])
|
|
|
|
|
|
} else if (modalType === 'edit' && selectedPayroll) {
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const updatedPayroll: HrPayroll = {
|
|
|
|
|
|
...selectedPayroll,
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
lastModificationTime: new Date(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
setPayrolls((prevPayrolls) =>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
prevPayrolls.map((p) => (p.id === selectedPayroll.id ? updatedPayroll : p)),
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setShowModal(false)
|
|
|
|
|
|
setSelectedPayroll(null)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
2025-09-15 19:46:52 +00:00
|
|
|
|
period: '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
baseSalary: 0,
|
|
|
|
|
|
overtime: 0,
|
|
|
|
|
|
bonus: 0,
|
|
|
|
|
|
allowances: [],
|
|
|
|
|
|
deductions: [],
|
|
|
|
|
|
status: PayrollStatusEnum.Draft,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setShowModal(false)
|
|
|
|
|
|
setSelectedPayroll(null)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
2025-09-15 19:46:52 +00:00
|
|
|
|
period: '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
baseSalary: 0,
|
|
|
|
|
|
overtime: 0,
|
|
|
|
|
|
bonus: 0,
|
|
|
|
|
|
allowances: [],
|
|
|
|
|
|
deductions: [],
|
|
|
|
|
|
status: PayrollStatusEnum.Draft,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Calculate gross and net salary helper function
|
|
|
|
|
|
const calculateSalaries = (data: Partial<HrPayroll>) => {
|
|
|
|
|
|
const totalAllowances = (data.allowances || []).reduce(
|
|
|
|
|
|
(total, allowance) => total + allowance.amount,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
0,
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const totalDeductions = (data.deductions || []).reduce(
|
|
|
|
|
|
(total, deduction) => total + deduction.amount,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
0,
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const grossSalary =
|
2025-09-15 19:46:52 +00:00
|
|
|
|
(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
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
grossSalary,
|
|
|
|
|
|
tax,
|
|
|
|
|
|
socialSecurity,
|
|
|
|
|
|
netSalary,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Real-time calculation for form data
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const calculatedValues = calculateSalaries(formData)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const filteredPayrolls = payrolls.filter((payroll) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
if (selectedStatus !== 'all' && payroll.status !== selectedStatus) {
|
|
|
|
|
|
return false
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
if (selectedPeriod !== 'all' && payroll.period !== selectedPeriod) {
|
|
|
|
|
|
return false
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
if (selectedDepartment !== 'all' && payroll.employee?.department?.name !== selectedDepartment) {
|
|
|
|
|
|
return false
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
if (searchTerm.trim() !== '') {
|
2025-09-15 09:31:47 +00:00
|
|
|
|
if (payroll.employeeId !== searchTerm) {
|
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
|
|
|
|
return true
|
|
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
const columns: Column<HrPayroll>[] = [
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'employee',
|
|
|
|
|
|
header: 'Personel',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => (
|
|
|
|
|
|
<div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<div className="font-medium text-gray-900">{payroll.employee?.fullName}</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div className="text-sm text-gray-500">{payroll.employee?.code}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'period',
|
|
|
|
|
|
header: 'Dönem',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => payroll.period,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'baseSalary',
|
|
|
|
|
|
header: 'Temel Maaş',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => `₺${payroll.baseSalary.toLocaleString()}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'allowances',
|
|
|
|
|
|
header: 'Ödemeler',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => {
|
|
|
|
|
|
const totalAllowances = payroll.allowances.reduce(
|
|
|
|
|
|
(total, allowance) => total + allowance.amount,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
0,
|
|
|
|
|
|
)
|
|
|
|
|
|
return `₺${totalAllowances.toLocaleString()}`
|
2025-09-15 09:31:47 +00:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'deductions',
|
|
|
|
|
|
header: 'Kesintiler',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => {
|
|
|
|
|
|
const totalDeductions = payroll.deductions.reduce(
|
|
|
|
|
|
(total, deduction) => total + deduction.amount,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
0,
|
|
|
|
|
|
)
|
|
|
|
|
|
return `₺${totalDeductions.toLocaleString()}`
|
2025-09-15 09:31:47 +00:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'grossSalary',
|
|
|
|
|
|
header: 'Brüt Maaş',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => (
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<div className="font-medium text-gray-900">₺{payroll.grossSalary.toLocaleString()}</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'netSalary',
|
|
|
|
|
|
header: 'Net Maaş',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => (
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<div className="font-bold text-green-600">₺{payroll.netSalary.toLocaleString()}</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'status',
|
|
|
|
|
|
header: 'Durum',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
render: (payroll: HrPayroll) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getPayrollStatusColor(
|
2025-09-15 19:46:52 +00:00
|
|
|
|
payroll.status,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
)}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{getPayrollStatusText(payroll.status)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
2025-09-15 19:46:52 +00:00
|
|
|
|
key: 'actions',
|
|
|
|
|
|
header: 'İşlemler',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2025-09-15 19:46:52 +00:00
|
|
|
|
]
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// 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(
|
2025-09-15 19:46:52 +00:00
|
|
|
|
(p) => p.status === PayrollStatusEnum.Draft || p.status === PayrollStatusEnum.Calculated,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
).length,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Get unique periods for filter
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const periods = [...new Set(payrolls.map((p) => p.period))].sort().reverse()
|
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 items-center justify-between">
|
|
|
|
|
|
<div>
|
2025-09-15 21:02:48 +00:00
|
|
|
|
<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>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
</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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
|
|
|
|
|
|
{/* 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"
|
2025-09-15 09:31:47 +00:00
|
|
|
|
>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<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"
|
2025-09-15 09:31:47 +00:00
|
|
|
|
>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
<option value={PayrollStatusEnum.Draft}>Taslak</option>
|
|
|
|
|
|
<option value={PayrollStatusEnum.Calculated}>Hesaplandı</option>
|
|
|
|
|
|
<option value={PayrollStatusEnum.Approved}>Onaylandı</option>
|
|
|
|
|
|
<option value={PayrollStatusEnum.Paid}>Ödendi</option>
|
|
|
|
|
|
<option value={PayrollStatusEnum.Cancelled}>İptal Edildi</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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{/* Data Table */}
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow-sm border">
|
|
|
|
|
|
<DataTable data={filteredPayrolls} columns={columns} />
|
|
|
|
|
|
</div>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{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>
|
|
|
|
|
|
)}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</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">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType === 'add' && 'Yeni Bordro Ekle'}
|
|
|
|
|
|
{modalType === 'edit' && 'Bordro Düzenle'}
|
|
|
|
|
|
{modalType === 'view' && 'Bordro Detayları'}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</h3>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<button onClick={handleCancel} className="text-gray-400 hover:text-gray-600">
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<FaTimes className="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* Employee Selection */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType === 'add' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Personel</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<select
|
2025-09-15 19:46:52 +00:00
|
|
|
|
value={formData.employeeId || ''}
|
|
|
|
|
|
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
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) */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{(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}
|
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
|
|
|
|
|
|
|
|
|
|
{/* Period */}
|
|
|
|
|
|
<div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Dönem</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<input
|
|
|
|
|
|
type="month"
|
|
|
|
|
|
value={formData.period}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
onChange={(e) => setFormData({ ...formData, period: e.target.value })}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
2025-09-15 19:46:52 +00:00
|
|
|
|
disabled={modalType === 'view'}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</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"
|
2025-09-15 19:46:52 +00:00
|
|
|
|
disabled={modalType === 'view'}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</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"
|
2025-09-15 19:46:52 +00:00
|
|
|
|
disabled={modalType === 'view'}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Bonus */}
|
|
|
|
|
|
<div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Prim (₺)</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={formData.bonus}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
onChange={(e) => setFormData({ ...formData, bonus: Number(e.target.value) })}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
2025-09-15 19:46:52 +00:00
|
|
|
|
disabled={modalType === 'view'}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Allowances */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType !== 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div>
|
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700">Ödemeler</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newAllowances = [
|
|
|
|
|
|
...(formData.allowances || []),
|
|
|
|
|
|
{
|
|
|
|
|
|
id: Date.now().toString(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
name: '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
amount: 0,
|
|
|
|
|
|
taxable: true,
|
|
|
|
|
|
},
|
2025-09-15 19:46:52 +00:00
|
|
|
|
]
|
|
|
|
|
|
setFormData({ ...formData, allowances: newAllowances })
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
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) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const updatedAllowances = [...(formData.allowances || [])]
|
2025-09-15 09:31:47 +00:00
|
|
|
|
updatedAllowances[index] = {
|
|
|
|
|
|
...allowance,
|
|
|
|
|
|
name: e.target.value,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
allowances: updatedAllowances,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
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) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const updatedAllowances = [...(formData.allowances || [])]
|
2025-09-15 09:31:47 +00:00
|
|
|
|
updatedAllowances[index] = {
|
|
|
|
|
|
...allowance,
|
|
|
|
|
|
amount: Number(e.target.value),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
allowances: updatedAllowances,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
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 =
|
2025-09-15 19:46:52 +00:00
|
|
|
|
formData.allowances?.filter((_, i) => i !== index) || []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
allowances: updatedAllowances,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
className="px-2 py-2 text-red-600 hover:text-red-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTimes className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{(!formData.allowances || formData.allowances.length === 0) && (
|
|
|
|
|
|
<p className="text-sm text-gray-500">Henüz ödeme eklenmedi</p>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Deductions */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType !== 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div>
|
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700">Kesintiler</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newDeductions = [
|
|
|
|
|
|
...(formData.deductions || []),
|
|
|
|
|
|
{
|
|
|
|
|
|
id: Date.now().toString(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
name: '',
|
2025-09-15 09:31:47 +00:00
|
|
|
|
amount: 0,
|
|
|
|
|
|
mandatory: false,
|
|
|
|
|
|
},
|
2025-09-15 19:46:52 +00:00
|
|
|
|
]
|
|
|
|
|
|
setFormData({ ...formData, deductions: newDeductions })
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
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) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const updatedDeductions = [...(formData.deductions || [])]
|
2025-09-15 09:31:47 +00:00
|
|
|
|
updatedDeductions[index] = {
|
|
|
|
|
|
...deduction,
|
|
|
|
|
|
name: e.target.value,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
deductions: updatedDeductions,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
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) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const updatedDeductions = [...(formData.deductions || [])]
|
2025-09-15 09:31:47 +00:00
|
|
|
|
updatedDeductions[index] = {
|
|
|
|
|
|
...deduction,
|
|
|
|
|
|
amount: Number(e.target.value),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
deductions: updatedDeductions,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
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 =
|
2025-09-15 19:46:52 +00:00
|
|
|
|
formData.deductions?.filter((_, i) => i !== index) || []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
deductions: updatedDeductions,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
className="px-2 py-2 text-red-600 hover:text-red-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTimes className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{(!formData.deductions || formData.deductions.length === 0) && (
|
|
|
|
|
|
<p className="text-sm text-gray-500">Henüz kesinti eklenmedi</p>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Real-time calculation preview for add/edit modes */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType !== 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<div className="bg-blue-50 p-4 rounded-lg">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<h5 className="font-medium text-gray-900 mb-3">Hesaplama Önizlemesi</h5>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
Temel: ₺{(formData.baseSalary || 0).toLocaleString()} + Ödemeler: ₺
|
2025-09-15 09:31:47 +00:00
|
|
|
|
{(formData.allowances || [])
|
2025-09-15 19:46:52 +00:00
|
|
|
|
.reduce((total, allowance) => total + allowance.amount, 0)
|
|
|
|
|
|
.toLocaleString()}{' '}
|
|
|
|
|
|
+ Mesai: ₺{(formData.overtime || 0).toLocaleString()} + Prim: ₺
|
|
|
|
|
|
{(formData.bonus || 0).toLocaleString()}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</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">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
Brüt - Vergi: ₺{calculatedValues.tax.toLocaleString()} - SGK: ₺
|
|
|
|
|
|
{calculatedValues.socialSecurity.toLocaleString()} - Kesintiler: ₺
|
2025-09-15 09:31:47 +00:00
|
|
|
|
{(formData.deductions || [])
|
2025-09-15 19:46:52 +00:00
|
|
|
|
.reduce((total, deduction) => total + deduction.amount, 0)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
.toLocaleString()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* View mode - show allowances and deductions */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType === 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<>
|
|
|
|
|
|
<div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Ödemeler</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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) => (
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<div key={allowance.id} className="flex justify-between text-sm">
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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
|
2025-09-15 19:46:52 +00:00
|
|
|
|
.reduce((total, allowance) => total + allowance.amount, 0)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
.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) => (
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<div key={deduction.id} className="flex justify-between text-sm">
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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
|
2025-09-15 19:46:52 +00:00
|
|
|
|
.reduce((total, deduction) => total + deduction.amount, 0)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
.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) */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType === 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<>
|
|
|
|
|
|
{/* Status */}
|
|
|
|
|
|
<div>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Durum</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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(
|
2025-09-15 19:46:52 +00:00
|
|
|
|
formData.status || PayrollStatusEnum.Draft,
|
2025-09-15 09:31:47 +00:00
|
|
|
|
)}`}
|
|
|
|
|
|
>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{getPayrollStatusText(formData.status || PayrollStatusEnum.Draft)}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</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 */}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType !== 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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" />
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType === 'add' ? 'Ekle' : 'Güncelle'}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
{modalType === 'view' && (
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setPayrolls((prev) => [...prev, ...payrolls])
|
|
|
|
|
|
setShowBulkModal(false)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
</Container>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Bulk Payroll Entry Component
|
|
|
|
|
|
interface BulkPayrollEntryProps {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
onClose: () => void
|
|
|
|
|
|
onSubmit: (payrolls: HrPayroll[]) => void
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const BulkPayrollEntry: React.FC<BulkPayrollEntryProps> = ({ onClose, onSubmit }) => {
|
|
|
|
|
|
const [selectedDepartment, setSelectedDepartment] = useState<string>('')
|
|
|
|
|
|
const [selectedEmployees, setSelectedEmployees] = useState<string[]>([])
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const [period, setPeriod] = useState<string>(
|
2025-09-15 19:46:52 +00:00
|
|
|
|
new Date().getFullYear() + '-' + String(new Date().getMonth() + 1).padStart(2, '0'),
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
const [bulkData, setBulkData] = useState({
|
|
|
|
|
|
baseSalaryIncrease: 0,
|
|
|
|
|
|
overtimeHours: 0,
|
|
|
|
|
|
overtimeRate: 50,
|
|
|
|
|
|
bonusAmount: 0,
|
|
|
|
|
|
massBonus: 0,
|
2025-09-15 19:46:52 +00:00
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Get unique departments from mockDepartments and employees
|
|
|
|
|
|
const departments = [
|
|
|
|
|
|
...new Set([
|
|
|
|
|
|
...mockDepartments.map((dept) => dept.name),
|
|
|
|
|
|
...mockEmployees.map((emp) => emp.department?.name).filter(Boolean),
|
|
|
|
|
|
]),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
].sort()
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Get employees by department
|
|
|
|
|
|
const filteredEmployees = selectedDepartment
|
|
|
|
|
|
? mockEmployees.filter((emp) => emp.department?.name === selectedDepartment)
|
2025-09-15 19:46:52 +00:00
|
|
|
|
: []
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Get selected employees data
|
|
|
|
|
|
const selectedEmployeesData = filteredEmployees.filter((emp) =>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
selectedEmployees.includes(emp.id),
|
|
|
|
|
|
)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Handle department change - reset selected employees and auto-select all
|
|
|
|
|
|
const handleDepartmentChange = (departmentName: string) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setSelectedDepartment(departmentName)
|
2025-09-15 09:31:47 +00:00
|
|
|
|
// Auto-select all employees when department is selected
|
|
|
|
|
|
if (departmentName) {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const employeesInDept = mockEmployees.filter((emp) => emp.department?.name === departmentName)
|
|
|
|
|
|
setSelectedEmployees(employeesInDept.map((emp) => emp.id))
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setSelectedEmployees([])
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Handle select all employees
|
|
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
|
|
|
|
if (checked) {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setSelectedEmployees(filteredEmployees.map((emp) => emp.id))
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setSelectedEmployees([])
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
// Handle individual employee selection
|
|
|
|
|
|
const handleEmployeeSelect = (employeeId: string, checked: boolean) => {
|
|
|
|
|
|
if (checked) {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setSelectedEmployees((prev) => [...prev, employeeId])
|
2025-09-15 09:31:47 +00:00
|
|
|
|
} else {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
setSelectedEmployees((prev) => prev.filter((id) => id !== employeeId))
|
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 handleBulkSubmit = () => {
|
|
|
|
|
|
if (!selectedDepartment || selectedEmployeesData.length === 0) {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
alert('Lütfen bir departman ve en az bir personel seçin')
|
|
|
|
|
|
return
|
2025-09-15 09:31:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newPayrolls: HrPayroll[] = selectedEmployeesData.map((employee) => {
|
2025-09-15 19:46:52 +00:00
|
|
|
|
const baseSalary = employee.baseSalary + bulkData.baseSalaryIncrease
|
|
|
|
|
|
const overtime = bulkData.overtimeHours * bulkData.overtimeRate
|
|
|
|
|
|
const bonus = bulkData.bonusAmount + bulkData.massBonus
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
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(),
|
2025-09-15 19:46:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
onSubmit(newPayrolls)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
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">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<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">
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Departman</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Dönem</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Mesai Saati</label>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
className={selectedEmployees.includes(employee.id) ? 'bg-blue-50' : ''}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
>
|
|
|
|
|
|
<td className="px-4 py-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={selectedEmployees.includes(employee.id)}
|
2025-09-15 19:46:52 +00:00
|
|
|
|
onChange={(e) => handleEmployeeSelect(employee.id, e.target.checked)}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</td>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<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>
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
₺{(employee.baseSalary + bulkData.baseSalaryIncrease).toLocaleString()}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
</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">
|
2025-09-15 19:46:52 +00:00
|
|
|
|
<strong>{selectedEmployees.length}</strong> personel seçildi. Toplam mevcut
|
|
|
|
|
|
maaş:{' '}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
<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>
|
2025-09-15 19:46:52 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-09-15 09:31:47 +00:00
|
|
|
|
|
2025-09-15 19:46:52 +00:00
|
|
|
|
export default PayrollManagement
|