1491 lines
54 KiB
TypeScript
1491 lines
54 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";
|
|||
|
|
|
|||
|
|
// 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 (
|
|||
|
|
<div className="space-y-4 pt-2">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div>
|
|||
|
|
<h2 className="text-xl font-bold text-gray-900">
|
|||
|
|
Maaş, Prim, Bordro Yönetimi
|
|||
|
|
</h2>
|
|||
|
|
<p className="text-gray-600 mt-1">
|
|||
|
|
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>
|
|||
|
|
<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>
|
|||
|
|
</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>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 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);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 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;
|