1240 lines
43 KiB
TypeScript
1240 lines
43 KiB
TypeScript
import React, { useState } from "react";
|
||
import {
|
||
FaClock,
|
||
FaUser,
|
||
FaPlus,
|
||
FaEdit,
|
||
FaCheck,
|
||
FaTimes,
|
||
FaEye,
|
||
FaUsers,
|
||
} from "react-icons/fa";
|
||
import { LeaveStatusEnum, HrOvertime } from "../../../types/hr";
|
||
import DataTable, { Column } from "../../../components/common/DataTable";
|
||
import { mockOvertimes } from "../../../mocks/mockOvertimes";
|
||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
||
import { mockDepartments } from "../../../mocks/mockDepartments";
|
||
import Widget from "../../../components/common/Widget";
|
||
import { getLeaveStatusColor, getLeaveStatusText } from "../../../utils/erp";
|
||
|
||
const OvertimeManagement: React.FC = () => {
|
||
// Overtime states
|
||
const [overtimes, setOvertimes] = useState<HrOvertime[]>(mockOvertimes);
|
||
const [overtimeSelectedStatus, setOvertimeSelectedStatus] =
|
||
useState<string>("all");
|
||
const [overtimeSelectedPeriod, setOvertimeSelectedPeriod] =
|
||
useState<string>("all");
|
||
const [searchOvertimesTerm, setSearchOvertimesTerm] = useState("");
|
||
const [selectedDepartment, setSelectedDepartment] = useState<string>("all");
|
||
|
||
// Overtime modal states
|
||
const [showOvertimeAddModal, setShowOvertimeAddModal] = useState(false);
|
||
const [showOvertimeEditModal, setShowOvertimeEditModal] = useState(false);
|
||
const [showOvertimeViewModal, setShowOvertimeViewModal] = useState(false);
|
||
const [showOvertimeRejectModal, setShowOvertimeRejectModal] = useState(false);
|
||
const [showBulkOvertimeModal, setShowBulkOvertimeModal] = useState(false);
|
||
const [selectedOvertime, setSelectedOvertime] = useState<HrOvertime | null>(
|
||
null
|
||
);
|
||
const [overtimeRejectReason, setOvertimeRejectReason] = useState("");
|
||
|
||
// Overtime form state
|
||
const [overtimeFormData, setOvertimeFormData] = useState({
|
||
employeeId: "",
|
||
date: "",
|
||
startTime: "",
|
||
endTime: "",
|
||
reason: "",
|
||
rate: 1.5,
|
||
});
|
||
|
||
// Bulk overtime form state
|
||
const [bulkOvertimeFormData, setBulkOvertimeFormData] = useState({
|
||
departmentId: "",
|
||
selectedEmployees: [] as string[],
|
||
date: "",
|
||
startTime: "",
|
||
endTime: "",
|
||
reason: "",
|
||
rate: 1.5,
|
||
});
|
||
|
||
// Get employees by department
|
||
const getEmployeesByDepartment = (departmentId: string) => {
|
||
return mockEmployees.filter((emp) => emp.departmantId === departmentId);
|
||
};
|
||
|
||
// Get selected employee object
|
||
const getSelectedEmployee = (employeeId: string) => {
|
||
return mockEmployees.find((emp) => emp.id === employeeId);
|
||
};
|
||
|
||
// Overtime handlers
|
||
const handleOvertimeAdd = () => {
|
||
setOvertimeFormData({
|
||
employeeId: "",
|
||
date: "",
|
||
startTime: "",
|
||
endTime: "",
|
||
reason: "",
|
||
rate: 1.5,
|
||
});
|
||
setShowOvertimeAddModal(true);
|
||
};
|
||
|
||
const handleOvertimeEdit = (overtime: HrOvertime) => {
|
||
setSelectedOvertime(overtime);
|
||
setOvertimeFormData({
|
||
employeeId: overtime.employeeId,
|
||
date: new Date(overtime.date).toISOString().split("T")[0],
|
||
startTime: overtime.startTime,
|
||
endTime: overtime.endTime,
|
||
reason: overtime.reason,
|
||
rate: overtime.rate || 1.5,
|
||
});
|
||
setShowOvertimeEditModal(true);
|
||
};
|
||
|
||
const handleOvertimeApprove = (id: string) => {
|
||
setOvertimes((prevOvertimes) =>
|
||
prevOvertimes.map((overtime) =>
|
||
overtime.id === id
|
||
? {
|
||
...overtime,
|
||
status: LeaveStatusEnum.Approved,
|
||
lastModificationTime: new Date(),
|
||
}
|
||
: overtime
|
||
)
|
||
);
|
||
alert("Mesai talebi onaylandı!");
|
||
};
|
||
|
||
const handleOvertimeReject = (id: string, reason?: string) => {
|
||
if (reason) {
|
||
setOvertimes((prevOvertimes) =>
|
||
prevOvertimes.map((overtime) =>
|
||
overtime.id === id
|
||
? {
|
||
...overtime,
|
||
status: LeaveStatusEnum.Rejected,
|
||
lastModificationTime: new Date(),
|
||
}
|
||
: overtime
|
||
)
|
||
);
|
||
setShowOvertimeRejectModal(false);
|
||
setOvertimeRejectReason("");
|
||
alert("Mesai talebi reddedildi!");
|
||
} else {
|
||
const overtime = overtimes.find((o) => o.id === id);
|
||
if (overtime) {
|
||
setSelectedOvertime(overtime);
|
||
setShowOvertimeRejectModal(true);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleOvertimeView = (overtime: HrOvertime) => {
|
||
setSelectedOvertime(overtime);
|
||
setShowOvertimeViewModal(true);
|
||
};
|
||
|
||
const handleSubmitOvertimeAdd = () => {
|
||
if (
|
||
!overtimeFormData.employeeId ||
|
||
!overtimeFormData.date ||
|
||
!overtimeFormData.startTime ||
|
||
!overtimeFormData.endTime
|
||
) {
|
||
alert("Lütfen tüm gerekli alanları doldurun!");
|
||
return;
|
||
}
|
||
|
||
const startTime = new Date(`2000-01-01 ${overtimeFormData.startTime}`);
|
||
const endTime = new Date(`2000-01-01 ${overtimeFormData.endTime}`);
|
||
const totalHours =
|
||
(endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
|
||
|
||
if (totalHours <= 0) {
|
||
alert("Bitiş saati başlangıç saatinden sonra olmalıdır!");
|
||
return;
|
||
}
|
||
|
||
const newOvertime: HrOvertime = {
|
||
id: `ot_${Date.now()}`,
|
||
employeeId: overtimeFormData.employeeId,
|
||
date: new Date(overtimeFormData.date),
|
||
startTime: overtimeFormData.startTime,
|
||
endTime: overtimeFormData.endTime,
|
||
totalHours: totalHours,
|
||
reason: overtimeFormData.reason,
|
||
status: LeaveStatusEnum.Pending,
|
||
rate: overtimeFormData.rate,
|
||
amount: totalHours * 100 * overtimeFormData.rate, // Assuming 100 TL/hour base rate
|
||
creationTime: new Date(),
|
||
lastModificationTime: new Date(),
|
||
};
|
||
|
||
setOvertimes((prevOvertimes) => [...prevOvertimes, newOvertime]);
|
||
setShowOvertimeAddModal(false);
|
||
alert("Mesai talebi başarıyla oluşturuldu!");
|
||
};
|
||
|
||
const handleSubmitOvertimeEdit = () => {
|
||
if (!selectedOvertime) return;
|
||
|
||
if (
|
||
!overtimeFormData.employeeId ||
|
||
!overtimeFormData.date ||
|
||
!overtimeFormData.startTime ||
|
||
!overtimeFormData.endTime
|
||
) {
|
||
alert("Lütfen tüm gerekli alanları doldurun!");
|
||
return;
|
||
}
|
||
|
||
const startTime = new Date(`2000-01-01 ${overtimeFormData.startTime}`);
|
||
const endTime = new Date(`2000-01-01 ${overtimeFormData.endTime}`);
|
||
const totalHours =
|
||
(endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
|
||
|
||
if (totalHours <= 0) {
|
||
alert("Bitiş saati başlangıç saatinden sonra olmalıdır!");
|
||
return;
|
||
}
|
||
|
||
setOvertimes((prevOvertimes) =>
|
||
prevOvertimes.map((overtime) =>
|
||
overtime.id === selectedOvertime.id
|
||
? {
|
||
...overtime,
|
||
employeeId: overtimeFormData.employeeId,
|
||
date: new Date(overtimeFormData.date),
|
||
startTime: overtimeFormData.startTime,
|
||
endTime: overtimeFormData.endTime,
|
||
totalHours: totalHours,
|
||
reason: overtimeFormData.reason,
|
||
rate: overtimeFormData.rate,
|
||
amount: totalHours * 100 * overtimeFormData.rate,
|
||
lastModificationTime: new Date(),
|
||
}
|
||
: overtime
|
||
)
|
||
);
|
||
|
||
setShowOvertimeEditModal(false);
|
||
setSelectedOvertime(null);
|
||
alert("Mesai talebi başarıyla güncellendi!");
|
||
};
|
||
|
||
const handleSubmitOvertimeReject = () => {
|
||
if (!selectedOvertime || !overtimeRejectReason.trim()) {
|
||
alert("Lütfen red nedeni giriniz!");
|
||
return;
|
||
}
|
||
|
||
handleOvertimeReject(selectedOvertime.id, overtimeRejectReason);
|
||
};
|
||
|
||
const handleBulkOvertimeAdd = () => {
|
||
setBulkOvertimeFormData({
|
||
departmentId: "",
|
||
selectedEmployees: [],
|
||
date: "",
|
||
startTime: "",
|
||
endTime: "",
|
||
reason: "",
|
||
rate: 1.5,
|
||
});
|
||
setShowBulkOvertimeModal(true);
|
||
};
|
||
|
||
const handleSubmitBulkOvertime = () => {
|
||
if (
|
||
!bulkOvertimeFormData.selectedEmployees.length ||
|
||
!bulkOvertimeFormData.date ||
|
||
!bulkOvertimeFormData.startTime ||
|
||
!bulkOvertimeFormData.endTime
|
||
) {
|
||
alert(
|
||
"Lütfen en az bir personel seçin ve tüm gerekli alanları doldurun!"
|
||
);
|
||
return;
|
||
}
|
||
|
||
const startTime = new Date(`2000-01-01 ${bulkOvertimeFormData.startTime}`);
|
||
const endTime = new Date(`2000-01-01 ${bulkOvertimeFormData.endTime}`);
|
||
const totalHours =
|
||
(endTime.getTime() - startTime.getTime()) / (1000 * 60 * 60);
|
||
|
||
if (totalHours <= 0) {
|
||
alert("Bitiş saati başlangıç saatinden sonra olmalıdır!");
|
||
return;
|
||
}
|
||
|
||
const newOvertimes: HrOvertime[] =
|
||
bulkOvertimeFormData.selectedEmployees.map((employeeId) => {
|
||
const employee = getSelectedEmployee(employeeId);
|
||
return {
|
||
id: `ot_${Date.now()}_${employeeId}`,
|
||
employeeId: employeeId,
|
||
employee: employee,
|
||
date: new Date(bulkOvertimeFormData.date),
|
||
startTime: bulkOvertimeFormData.startTime,
|
||
endTime: bulkOvertimeFormData.endTime,
|
||
totalHours: totalHours,
|
||
reason: bulkOvertimeFormData.reason,
|
||
status: LeaveStatusEnum.Pending,
|
||
rate: bulkOvertimeFormData.rate,
|
||
amount: totalHours * 100 * bulkOvertimeFormData.rate, // 100 TL/hour base rate
|
||
creationTime: new Date(),
|
||
lastModificationTime: new Date(),
|
||
};
|
||
});
|
||
|
||
setOvertimes((prevOvertimes) => [...prevOvertimes, ...newOvertimes]);
|
||
setShowBulkOvertimeModal(false);
|
||
setBulkOvertimeFormData({
|
||
departmentId: "",
|
||
selectedEmployees: [],
|
||
date: "",
|
||
startTime: "",
|
||
endTime: "",
|
||
reason: "",
|
||
rate: 1.5,
|
||
});
|
||
alert(
|
||
`${newOvertimes.length} personel için toplu mesai talebi başarıyla oluşturuldu!`
|
||
);
|
||
};
|
||
|
||
// Filter overtime data
|
||
const filteredOvertimes = overtimes.filter((overtime) => {
|
||
if (
|
||
overtimeSelectedStatus !== "all" &&
|
||
overtime.status !== overtimeSelectedStatus
|
||
) {
|
||
return false;
|
||
}
|
||
if (
|
||
selectedDepartment !== "all" &&
|
||
overtime.employee?.department?.name !== selectedDepartment
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
if (overtimeSelectedPeriod !== "all") {
|
||
const now = new Date();
|
||
const overtimeDate = new Date(overtime.date);
|
||
|
||
switch (overtimeSelectedPeriod) {
|
||
case "this-month": {
|
||
const currentMonth = now.getMonth();
|
||
const currentYear = now.getFullYear();
|
||
return (
|
||
overtimeDate.getMonth() === currentMonth &&
|
||
overtimeDate.getFullYear() === currentYear
|
||
);
|
||
}
|
||
case "last-month": {
|
||
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1);
|
||
return (
|
||
overtimeDate.getMonth() === lastMonth.getMonth() &&
|
||
overtimeDate.getFullYear() === lastMonth.getFullYear()
|
||
);
|
||
}
|
||
case "last-3-months": {
|
||
const threeMonthsAgo = new Date(
|
||
now.getFullYear(),
|
||
now.getMonth() - 3
|
||
);
|
||
return overtimeDate >= threeMonthsAgo;
|
||
}
|
||
default:
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Search filter
|
||
if (searchOvertimesTerm.trim() !== "") {
|
||
if (overtime.employeeId !== searchOvertimesTerm) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
// Overtime table columns
|
||
const overtimeColumns: Column<HrOvertime>[] = [
|
||
{
|
||
key: "employee",
|
||
header: "Personel",
|
||
render: (overtime: HrOvertime) => {
|
||
const employee = mockEmployees.find(
|
||
(emp) => emp.id === overtime.employeeId
|
||
);
|
||
return (
|
||
<div className="flex items-center gap-2">
|
||
<FaUser className="w-4 h-4 text-gray-500" />
|
||
<div>
|
||
<div className="font-medium text-gray-900">
|
||
{employee?.fullName}
|
||
</div>
|
||
<div className="text-sm text-gray-500">{employee?.code}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
key: "date",
|
||
header: "Tarih",
|
||
render: (overtime: HrOvertime) => (
|
||
<div className="text-sm">
|
||
{new Date(overtime.date).toLocaleDateString("tr-TR")}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "time",
|
||
header: "Mesai Saatleri",
|
||
render: (overtime: HrOvertime) => (
|
||
<div className="text-sm">
|
||
<div>
|
||
{overtime.startTime} - {overtime.endTime}
|
||
</div>
|
||
<div className="text-gray-500">{overtime.totalHours} saat</div>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "reason",
|
||
header: "Neden",
|
||
render: (overtime: HrOvertime) => (
|
||
<div className="text-sm max-w-xs truncate" title={overtime.reason}>
|
||
{overtime.reason}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "rate",
|
||
header: "Çarpan",
|
||
render: (overtime: HrOvertime) => (
|
||
<div className="text-sm">x{overtime.rate}</div>
|
||
),
|
||
},
|
||
{
|
||
key: "amount",
|
||
header: "Tutar",
|
||
render: (overtime: HrOvertime) => (
|
||
<div className="text-sm font-medium">
|
||
{overtime.amount?.toLocaleString("tr-TR")} ₺
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: "status",
|
||
header: "Durum",
|
||
render: (overtime: HrOvertime) => (
|
||
<span
|
||
className={`px-2 py-1 text-xs font-medium rounded-full ${getLeaveStatusColor(
|
||
overtime.status
|
||
)}`}
|
||
>
|
||
{getLeaveStatusText(overtime.status)}
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
key: "actions",
|
||
header: "İşlemler",
|
||
render: (overtime: HrOvertime) => (
|
||
<div className="flex gap-1">
|
||
<button
|
||
onClick={() => handleOvertimeView(overtime)}
|
||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||
title="Görüntüle"
|
||
>
|
||
<FaEye className="w-4 h-4" />
|
||
</button>
|
||
|
||
{overtime.status === LeaveStatusEnum.Pending && (
|
||
<>
|
||
<button
|
||
onClick={() => handleOvertimeApprove(overtime.id)}
|
||
className="p-1 text-green-600 hover:bg-green-50 rounded"
|
||
title="Onayla"
|
||
>
|
||
<FaCheck className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleOvertimeReject(overtime.id)}
|
||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||
title="Reddet"
|
||
>
|
||
<FaTimes className="w-4 h-4" />
|
||
</button>
|
||
</>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => handleOvertimeEdit(overtime)}
|
||
className="p-1 text-gray-600 hover:bg-gray-50 rounded"
|
||
title="Düzenle"
|
||
>
|
||
<FaEdit className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-3 pt-2">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-gray-900">Mesai Yönetimi</h2>
|
||
<p className="text-sm text-gray-600 mt-1">
|
||
Personel mesai talepleri ve onay süreçleri
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={handleOvertimeAdd}
|
||
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 Mesai Talebi
|
||
</button>
|
||
<button
|
||
onClick={handleBulkOvertimeAdd}
|
||
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 Mesai
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<Widget
|
||
title="Toplam Mesai"
|
||
value={overtimes.length}
|
||
color="blue"
|
||
icon="FaClock"
|
||
/>
|
||
|
||
<Widget
|
||
title="Beklemede"
|
||
value={
|
||
overtimes.filter((o) => o.status === LeaveStatusEnum.Pending).length
|
||
}
|
||
color="yellow"
|
||
icon="FaClock"
|
||
/>
|
||
|
||
<Widget
|
||
title="Onaylanan"
|
||
value={
|
||
overtimes.filter((o) => o.status === LeaveStatusEnum.Approved)
|
||
.length
|
||
}
|
||
color="green"
|
||
icon="FaCheck"
|
||
/>
|
||
|
||
<Widget
|
||
title="Toplam Tutar"
|
||
value={`${overtimes
|
||
.filter((o) => o.status === LeaveStatusEnum.Approved)
|
||
.reduce((sum, o) => sum + (o.amount || 0), 0)
|
||
.toLocaleString("tr-TR")} ₺`}
|
||
color="purple"
|
||
icon="FaDollarSign"
|
||
/>
|
||
</div>
|
||
|
||
{/* Filters */}
|
||
<div className="flex gap-3 items-center">
|
||
<select
|
||
value={selectedDepartment}
|
||
onChange={(e) => setSelectedDepartment(e.target.value)}
|
||
className="px-3 py-1.5 border border-gray-300 rounded-md text-sm 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={searchOvertimesTerm}
|
||
onChange={(e) => setSearchOvertimesTerm(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={overtimeSelectedStatus}
|
||
onChange={(e) => setOvertimeSelectedStatus(e.target.value)}
|
||
className="border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
>
|
||
<option value="all">Tüm Durumlar</option>
|
||
<option value={LeaveStatusEnum.Pending}>Beklemede</option>
|
||
<option value={LeaveStatusEnum.Approved}>Onaylı</option>
|
||
<option value={LeaveStatusEnum.Rejected}>Reddedildi</option>
|
||
</select>
|
||
|
||
<select
|
||
value={overtimeSelectedPeriod}
|
||
onChange={(e) => setOvertimeSelectedPeriod(e.target.value)}
|
||
className="border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||
>
|
||
<option value="all">Tüm Dönemler</option>
|
||
<option value="this-month">Bu Ay</option>
|
||
<option value="last-month">Geçen Ay</option>
|
||
<option value="last-3-months">Son 3 Ay</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Data Table */}
|
||
<div className="bg-white rounded-lg shadow-sm border">
|
||
<DataTable data={filteredOvertimes} columns={overtimeColumns} />
|
||
</div>
|
||
|
||
{filteredOvertimes.length === 0 && (
|
||
<div className="text-center py-12">
|
||
<FaClock className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||
Mesai talebi bulunamadı
|
||
</h3>
|
||
<p className="text-gray-500">
|
||
Seçilen kriterlere uygun mesai talebi bulunmamaktadır.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Overtime Add/Edit Modal */}
|
||
{(showOvertimeAddModal || showOvertimeEditModal) && selectedOvertime && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-4 w-full max-w-md">
|
||
<h3 className="text-base font-semibold mb-4">
|
||
{showOvertimeAddModal
|
||
? "Yeni Mesai Talebi"
|
||
: "Mesai Talebini Düzenle"}
|
||
</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Personel
|
||
</label>
|
||
<select
|
||
value={overtimeFormData.employeeId}
|
||
onChange={(e) =>
|
||
setOvertimeFormData({
|
||
...overtimeFormData,
|
||
employeeId: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 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>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Tarih
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={overtimeFormData.date}
|
||
onChange={(e) =>
|
||
setOvertimeFormData({
|
||
...overtimeFormData,
|
||
date: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Başlangıç Saati
|
||
</label>
|
||
<input
|
||
type="time"
|
||
value={overtimeFormData.startTime}
|
||
onChange={(e) =>
|
||
setOvertimeFormData({
|
||
...overtimeFormData,
|
||
startTime: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm 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">
|
||
Bitiş Saati
|
||
</label>
|
||
<input
|
||
type="time"
|
||
value={overtimeFormData.endTime}
|
||
onChange={(e) =>
|
||
setOvertimeFormData({
|
||
...overtimeFormData,
|
||
endTime: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Mesai Çarpanı
|
||
</label>
|
||
<select
|
||
value={overtimeFormData.rate}
|
||
onChange={(e) =>
|
||
setOvertimeFormData({
|
||
...overtimeFormData,
|
||
rate: parseFloat(e.target.value),
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value={1.5}>x1.5 (Normal Mesai)</option>
|
||
<option value={2.0}>x2.0 (Hafta Sonu/Tatil)</option>
|
||
<option value={2.5}>x2.5 (Resmi Tatil)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Neden
|
||
</label>
|
||
<textarea
|
||
value={overtimeFormData.reason}
|
||
onChange={(e) =>
|
||
setOvertimeFormData({
|
||
...overtimeFormData,
|
||
reason: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
rows={3}
|
||
placeholder="Mesai nedenini belirtiniz..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-2 mt-6">
|
||
<button
|
||
onClick={() => {
|
||
setShowOvertimeAddModal(false);
|
||
setShowOvertimeEditModal(false);
|
||
setSelectedOvertime(null);
|
||
}}
|
||
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
İptal
|
||
</button>
|
||
<button
|
||
onClick={
|
||
showOvertimeAddModal
|
||
? handleSubmitOvertimeAdd
|
||
: handleSubmitOvertimeEdit
|
||
}
|
||
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||
>
|
||
{showOvertimeAddModal ? "Oluştur" : "Güncelle"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Overtime View Modal */}
|
||
{showOvertimeViewModal && selectedOvertime && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-4 w-full max-w-lg">
|
||
<h3 className="text-base font-semibold mb-4">Mesai Detayları</h3>
|
||
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Personel
|
||
</label>
|
||
<p className="text-gray-900">
|
||
{mockEmployees.find(
|
||
(emp) => emp.id === selectedOvertime.employeeId
|
||
)?.fullName || selectedOvertime.employeeId}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Tarih
|
||
</label>
|
||
<p className="text-gray-900">
|
||
{new Date(selectedOvertime.date).toLocaleDateString(
|
||
"tr-TR"
|
||
)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Başlangıç Saati
|
||
</label>
|
||
<p className="text-gray-900">{selectedOvertime.startTime}</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Bitiş Saati
|
||
</label>
|
||
<p className="text-gray-900">{selectedOvertime.endTime}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Toplam Saat
|
||
</label>
|
||
<p className="text-gray-900">
|
||
{selectedOvertime.totalHours} saat
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Çarpan
|
||
</label>
|
||
<p className="text-gray-900">x{selectedOvertime.rate}</p>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Tutar
|
||
</label>
|
||
<p className="text-gray-900 font-medium">
|
||
{selectedOvertime.amount?.toLocaleString("tr-TR")} ₺
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Durum
|
||
</label>
|
||
<span
|
||
className={`px-2 py-1 text-xs font-medium rounded-full ${getLeaveStatusColor(
|
||
selectedOvertime.status
|
||
)}`}
|
||
>
|
||
{getLeaveStatusText(selectedOvertime.status)}
|
||
</span>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Neden
|
||
</label>
|
||
<p className="text-gray-900">{selectedOvertime.reason}</p>
|
||
</div>
|
||
|
||
{selectedOvertime.approvedBy && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700">
|
||
Onaylayan
|
||
</label>
|
||
<p className="text-gray-900">
|
||
{
|
||
mockEmployees.find(
|
||
(emp) => emp.id === selectedOvertime.approvedBy
|
||
)?.fullName
|
||
}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex justify-end mt-6">
|
||
<button
|
||
onClick={() => {
|
||
setShowOvertimeViewModal(false);
|
||
setSelectedOvertime(null);
|
||
}}
|
||
className="px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
||
>
|
||
Kapat
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Overtime Reject Modal */}
|
||
{showOvertimeRejectModal && selectedOvertime && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-4 w-full max-w-md">
|
||
<h3 className="text-base font-semibold mb-4">
|
||
Mesai Talebini Reddet
|
||
</h3>
|
||
|
||
<div className="mb-4">
|
||
<p className="text-gray-700 mb-2">
|
||
<strong>
|
||
{mockEmployees.find(
|
||
(emp) => emp.id === selectedOvertime.employeeId
|
||
)?.fullName || selectedOvertime.employeeId}
|
||
</strong>{" "}
|
||
adlı personelin mesai talebini reddetmek üzeresiniz.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="mb-4">
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Red Nedeni <span className="text-red-500">*</span>
|
||
</label>
|
||
<textarea
|
||
value={overtimeRejectReason}
|
||
onChange={(e) => setOvertimeRejectReason(e.target.value)}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
rows={3}
|
||
placeholder="Reddedilme nedenini belirtiniz..."
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-2">
|
||
<button
|
||
onClick={() => {
|
||
setShowOvertimeRejectModal(false);
|
||
setSelectedOvertime(null);
|
||
setOvertimeRejectReason("");
|
||
}}
|
||
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
İptal
|
||
</button>
|
||
<button
|
||
onClick={handleSubmitOvertimeReject}
|
||
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded-md hover:bg-red-700"
|
||
>
|
||
Reddet
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Bulk Overtime Modal */}
|
||
{showBulkOvertimeModal && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-4 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
<h3 className="text-base font-semibold mb-4">Toplu Mesai Talebi</h3>
|
||
|
||
<div className="space-y-4">
|
||
{/* Department Selection */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Departman
|
||
</label>
|
||
<select
|
||
value={bulkOvertimeFormData.departmentId}
|
||
onChange={(e) => {
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
departmentId: e.target.value,
|
||
selectedEmployees: [],
|
||
});
|
||
}}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value="">Departman seçiniz...</option>
|
||
{mockDepartments.map((department) => (
|
||
<option key={department.id} value={department.id}>
|
||
{department.name} ({department.code})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Employee Selection */}
|
||
{bulkOvertimeFormData.departmentId && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Personeller
|
||
</label>
|
||
<div className="border border-gray-300 rounded-md p-3 max-h-40 overflow-y-auto">
|
||
<div className="space-y-2">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={
|
||
getEmployeesByDepartment(
|
||
bulkOvertimeFormData.departmentId
|
||
).length > 0 &&
|
||
bulkOvertimeFormData.selectedEmployees.length ===
|
||
getEmployeesByDepartment(
|
||
bulkOvertimeFormData.departmentId
|
||
).length
|
||
}
|
||
onChange={(e) => {
|
||
const departmentEmployees =
|
||
getEmployeesByDepartment(
|
||
bulkOvertimeFormData.departmentId
|
||
);
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
selectedEmployees: e.target.checked
|
||
? departmentEmployees.map((emp) => emp.id)
|
||
: [],
|
||
});
|
||
}}
|
||
className="mr-2"
|
||
/>
|
||
<span className="font-medium">Tümünü Seç</span>
|
||
</label>
|
||
{getEmployeesByDepartment(
|
||
bulkOvertimeFormData.departmentId
|
||
).map((employee) => (
|
||
<label key={employee.id} className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={bulkOvertimeFormData.selectedEmployees.includes(
|
||
employee.id
|
||
)}
|
||
onChange={(e) => {
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
selectedEmployees: e.target.checked
|
||
? [
|
||
...bulkOvertimeFormData.selectedEmployees,
|
||
employee.id,
|
||
]
|
||
: bulkOvertimeFormData.selectedEmployees.filter(
|
||
(id) => id !== employee.id
|
||
),
|
||
});
|
||
}}
|
||
className="mr-2"
|
||
/>
|
||
<span>
|
||
{employee.fullName} ({employee.code})
|
||
</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
{bulkOvertimeFormData.selectedEmployees.length} personel
|
||
seçildi
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Date */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Mesai Tarihi
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={bulkOvertimeFormData.date}
|
||
onChange={(e) =>
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
date: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
|
||
{/* Time Range */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Başlangıç Saati
|
||
</label>
|
||
<input
|
||
type="time"
|
||
value={bulkOvertimeFormData.startTime}
|
||
onChange={(e) =>
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
startTime: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm 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">
|
||
Bitiş Saati
|
||
</label>
|
||
<input
|
||
type="time"
|
||
value={bulkOvertimeFormData.endTime}
|
||
onChange={(e) =>
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
endTime: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Rate */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Mesai Çarpanı
|
||
</label>
|
||
<select
|
||
value={bulkOvertimeFormData.rate}
|
||
onChange={(e) =>
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
rate: parseFloat(e.target.value),
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
>
|
||
<option value={1.5}>x1.5 (Normal Mesai)</option>
|
||
<option value={2.0}>x2.0 (Hafta Sonu/Tatil)</option>
|
||
<option value={2.5}>x2.5 (Resmi Tatil)</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Reason */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
Neden (Opsiyonel)
|
||
</label>
|
||
<textarea
|
||
value={bulkOvertimeFormData.reason}
|
||
onChange={(e) =>
|
||
setBulkOvertimeFormData({
|
||
...bulkOvertimeFormData,
|
||
reason: e.target.value,
|
||
})
|
||
}
|
||
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
rows={3}
|
||
placeholder="Mesai nedenini belirtiniz..."
|
||
/>
|
||
</div>
|
||
|
||
{/* Summary */}
|
||
{bulkOvertimeFormData.selectedEmployees.length > 0 &&
|
||
bulkOvertimeFormData.startTime &&
|
||
bulkOvertimeFormData.endTime && (
|
||
<div className="bg-blue-50 p-3 rounded-lg">
|
||
<h4 className="text-sm font-medium text-blue-900 mb-2">
|
||
Özet
|
||
</h4>
|
||
<div className="text-sm text-blue-800 space-y-1">
|
||
<p>
|
||
<span className="font-medium">Personel Sayısı:</span>{" "}
|
||
{bulkOvertimeFormData.selectedEmployees.length}
|
||
</p>
|
||
{(() => {
|
||
const startTime = new Date(
|
||
`2000-01-01 ${bulkOvertimeFormData.startTime}`
|
||
);
|
||
const endTime = new Date(
|
||
`2000-01-01 ${bulkOvertimeFormData.endTime}`
|
||
);
|
||
const totalHours =
|
||
(endTime.getTime() - startTime.getTime()) /
|
||
(1000 * 60 * 60);
|
||
const totalAmount =
|
||
totalHours *
|
||
100 *
|
||
bulkOvertimeFormData.rate *
|
||
bulkOvertimeFormData.selectedEmployees.length;
|
||
|
||
return totalHours > 0 ? (
|
||
<>
|
||
<p>
|
||
<span className="font-medium">Mesai Süresi:</span>{" "}
|
||
{totalHours} saat
|
||
</p>
|
||
<p>
|
||
<span className="font-medium">Çarpan:</span> x
|
||
{bulkOvertimeFormData.rate}
|
||
</p>
|
||
<p>
|
||
<span className="font-medium">Toplam Tutar:</span>{" "}
|
||
{totalAmount.toLocaleString("tr-TR")} ₺
|
||
</p>
|
||
</>
|
||
) : null;
|
||
})()}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-2 mt-6">
|
||
<button
|
||
onClick={() => {
|
||
setShowBulkOvertimeModal(false);
|
||
setBulkOvertimeFormData({
|
||
departmentId: "",
|
||
selectedEmployees: [],
|
||
date: "",
|
||
startTime: "",
|
||
endTime: "",
|
||
reason: "",
|
||
rate: 1.5,
|
||
});
|
||
}}
|
||
className="px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50"
|
||
>
|
||
İptal
|
||
</button>
|
||
<button
|
||
onClick={handleSubmitBulkOvertime}
|
||
className="px-3 py-1.5 text-sm bg-green-600 text-white rounded-md hover:bg-green-700"
|
||
>
|
||
Toplu Mesai Oluştur
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default OvertimeManagement;
|