erp-platform/ui/src/views/hr/components/PayrollManagement.tsx

1491 lines
54 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
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;