Hr menusüde kullanılan komponentler silindi
This commit is contained in:
parent
cdcf9d309c
commit
f7deeb414d
24 changed files with 8 additions and 10547 deletions
|
|
@ -34291,7 +34291,7 @@ public class ListFormSeeder : IDataSeedContributor, ITransientDependency
|
|||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.Guid,
|
||||
FieldName = "EmploymentTypeId",
|
||||
Width = 100,
|
||||
Width = 200,
|
||||
ListOrderNo = 24,
|
||||
Visible = true,
|
||||
IsActive = true,
|
||||
|
|
|
|||
|
|
@ -888,55 +888,6 @@
|
|||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.employees",
|
||||
"path": "/admin/hr/employees",
|
||||
"componentPath": "@/views/hr/components/EmployeeList",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.employeesNew",
|
||||
"path": "/admin/hr/employees/new",
|
||||
"componentPath": "@/views/hr/components/EmployeeForm",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.employeesEdit",
|
||||
"path": "/admin/hr/employees/edit/:id",
|
||||
"componentPath": "@/views/hr/components/EmployeeForm",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.employeesDetail",
|
||||
"path": "/admin/hr/employees/:id",
|
||||
"componentPath": "@/views/hr/components/EmployeeView",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.departments",
|
||||
"path": "/admin/hr/departments",
|
||||
"componentPath": "@/views/hr/components/DepartmentManagement",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.jobPositions",
|
||||
"path": "/admin/hr/job-positions",
|
||||
"componentPath": "@/views/hr/components/JobPositions",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.employmentTypes",
|
||||
"path": "/admin/hr/employment-types",
|
||||
"componentPath": "@/views/hr/components/EmploymentTypes",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.organization",
|
||||
"path": "/admin/hr/organization",
|
||||
|
|
@ -944,41 +895,6 @@
|
|||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.badges",
|
||||
"path": "/admin/hr/badges",
|
||||
"componentPath": "@/views/hr/components/BadgeManagement",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.payroll",
|
||||
"path": "/admin/hr/payroll",
|
||||
"componentPath": "@/views/hr/components/PayrollManagement",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.costCenters",
|
||||
"path": "/admin/hr/cost-centers",
|
||||
"componentPath": "@/views/hr/components/CostCenterManagement",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.Template360s",
|
||||
"path": "/admin/hr/360-templates",
|
||||
"componentPath": "@/views/hr/components/Degree360Templates",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.hr.evaluation",
|
||||
"path": "/admin/hr/360-evaluation",
|
||||
"componentPath": "@/views/hr/components/Degree360Evaluation",
|
||||
"routeType": "protected",
|
||||
"authority": null
|
||||
},
|
||||
{
|
||||
"key": "admin.crm.customers",
|
||||
"path": "/admin/crm/customers",
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ using Microsoft.EntityFrameworkCore;
|
|||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Volo.Abp.Identity;
|
||||
using Volo.Abp.Timing;
|
||||
|
||||
namespace Kurs.Platform.Data.Seeds;
|
||||
|
||||
public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||
{
|
||||
private readonly IClock _clock;
|
||||
private readonly IRepository<IdentityUser, Guid> _repositoryUser;
|
||||
private readonly IRepository<GlobalSearch, int> _globalSearch;
|
||||
private readonly IRepository<CustomEndpoint, Guid> _customEndpointRepository;
|
||||
|
|
@ -94,6 +96,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
private readonly IRepository<MaterialGroup, Guid> _materialGroupRepository;
|
||||
|
||||
public TenantDataSeeder(
|
||||
IClock clock,
|
||||
IRepository<IdentityUser, Guid> repositoryUser,
|
||||
IRepository<GlobalSearch, int> globalSearch,
|
||||
IRepository<Sector, Guid> sectorRepository,
|
||||
|
|
@ -170,6 +173,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
IRepository<MaterialGroup, Guid> materialGroupRepository
|
||||
)
|
||||
{
|
||||
_clock = clock;
|
||||
_repositoryUser = repositoryUser;
|
||||
_globalSearch = globalSearch;
|
||||
_sectorRepository = sectorRepository;
|
||||
|
|
@ -1095,6 +1099,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
IsHalfDay = item.IsHalfDay,
|
||||
Reason = item.Reason,
|
||||
Status = item.Status,
|
||||
AppliedDate = _clock.Now,
|
||||
}, autoSave: true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { FaChevronDown, FaTimes } from "react-icons/fa";
|
||||
import { mockEmployees } from "../../mocks/mockEmployees";
|
||||
import { EmployeeDto } from "../../types/hr";
|
||||
import { EmployeeDto } from "@/proxy/intranet/models";
|
||||
|
||||
interface MultiSelectEmployeeProps {
|
||||
selectedEmployees: string[];
|
||||
|
|
|
|||
|
|
@ -1,336 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
FaUsers,
|
||||
FaAward,
|
||||
FaSave,
|
||||
FaTimesCircle,
|
||||
FaCalendarAlt,
|
||||
} from "react-icons/fa";
|
||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
||||
import { mockBadges } from "../../../mocks/mockBadges";
|
||||
|
||||
export interface BadgeAssignmentFormData {
|
||||
employeeId: string;
|
||||
badgeId: string;
|
||||
reason?: string;
|
||||
earnedDate: string;
|
||||
expiryDate?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface BadgeAssignmentModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (assignmentData: BadgeAssignmentFormData) => void;
|
||||
preSelectedEmployeeId?: string;
|
||||
preSelectedBadgeId?: string;
|
||||
}
|
||||
|
||||
const BadgeAssignmentModal: React.FC<BadgeAssignmentModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
preSelectedEmployeeId,
|
||||
preSelectedBadgeId,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<BadgeAssignmentFormData>({
|
||||
employeeId: "",
|
||||
badgeId: "",
|
||||
reason: "",
|
||||
earnedDate: new Date().toISOString().split("T")[0],
|
||||
expiryDate: "",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const [searchEmployee, setSearchEmployee] = useState("");
|
||||
const [searchBadge, setSearchBadge] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFormData({
|
||||
employeeId: preSelectedEmployeeId || "",
|
||||
badgeId: preSelectedBadgeId || "",
|
||||
reason: "",
|
||||
earnedDate: new Date().toISOString().split("T")[0],
|
||||
expiryDate: "",
|
||||
notes: "",
|
||||
});
|
||||
setSearchEmployee("");
|
||||
setSearchBadge("");
|
||||
}
|
||||
}, [isOpen, preSelectedEmployeeId, preSelectedBadgeId]);
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const filteredEmployees = mockEmployees.filter(
|
||||
(employee) =>
|
||||
employee.fullName.toLowerCase().includes(searchEmployee.toLowerCase()) ||
|
||||
employee.code.toLowerCase().includes(searchEmployee.toLowerCase())
|
||||
);
|
||||
|
||||
const filteredBadges = mockBadges.filter(
|
||||
(badge) =>
|
||||
badge.isActive &&
|
||||
(badge.name.toLowerCase().includes(searchBadge.toLowerCase()) ||
|
||||
badge.description.toLowerCase().includes(searchBadge.toLowerCase()))
|
||||
);
|
||||
|
||||
const selectedEmployee = mockEmployees.find(
|
||||
(emp) => emp.id === formData.employeeId
|
||||
);
|
||||
const selectedBadge = mockBadges.find(
|
||||
(badge) => badge.id === formData.badgeId
|
||||
);
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-3 border-b border-gray-200 bg-gradient-to-r from-green-50 to-emerald-50 rounded-t-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-600 rounded-lg flex items-center justify-center">
|
||||
<FaAward className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-gray-900">Rozet Ata</h3>
|
||||
<p className="text-gray-600">
|
||||
Personele başarı rozeti atayın
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<FaTimesCircle className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-3">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Sol Kolon - Personel Seçimi */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-2 rounded-lg">
|
||||
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<FaUsers className="w-5 h-5 text-blue-600" />
|
||||
Personel Seçimi
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<select
|
||||
name="employeeId"
|
||||
value={formData.employeeId}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
required
|
||||
>
|
||||
<option value="">Personel seçin...</option>
|
||||
{filteredEmployees.map((employee) => (
|
||||
<option key={employee.id} value={employee.id}>
|
||||
{employee.fullName} - {employee.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedEmployee && (
|
||||
<div className="bg-blue-50 p-2 rounded-lg border border-blue-200 text-xs">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-blue-900 mb-1">
|
||||
{selectedEmployee.fullName}
|
||||
</h4>
|
||||
<p className="text-xs text-blue-700 mb-1">
|
||||
{selectedEmployee.code} •{" "}
|
||||
{selectedEmployee.department?.name}
|
||||
</p>
|
||||
<div className="text-xs text-blue-600">
|
||||
<span className="font-medium">Email:</span>{" "}
|
||||
{selectedEmployee.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sağ Kolon - Rozet Seçimi */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-2 rounded-lg">
|
||||
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<FaAward className="w-5 h-5 text-purple-600" />
|
||||
Rozet Seçimi
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<select
|
||||
name="badgeId"
|
||||
value={formData.badgeId}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
required
|
||||
>
|
||||
<option value="">Rozet seçin...</option>
|
||||
{filteredBadges.map((badge) => (
|
||||
<option key={badge.id} value={badge.id}>
|
||||
{badge.icon} {badge.name} ({badge.points} puan)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedBadge && (
|
||||
<div className="bg-purple-50 p-2 rounded-lg border border-purple-200 text-xs">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-lg flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: selectedBadge.backgroundColor,
|
||||
border: `2px solid ${selectedBadge.color}`,
|
||||
}}
|
||||
>
|
||||
{selectedBadge.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-purple-900 mb-1">
|
||||
{selectedBadge.name}
|
||||
</h4>
|
||||
<p className="text-xs text-purple-700 mb-1">
|
||||
{selectedBadge.description}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-purple-600">
|
||||
<div>
|
||||
<span className="font-medium">Puan:</span>{" "}
|
||||
{selectedBadge.points}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Kategori:</span>{" "}
|
||||
{selectedBadge.category}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-purple-600">
|
||||
<span className="font-medium">Kriter:</span>{" "}
|
||||
{selectedBadge.criteria}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Atama Detayları */}
|
||||
<div className="mt-3 bg-gray-50 p-2 rounded-lg">
|
||||
<h4 className="text-sm font-semibold text-gray-800 mb-2 flex items-center gap-2">
|
||||
<FaCalendarAlt className="w-5 h-5 text-green-600" />
|
||||
Atama Detayları
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Kazanılma Tarihi <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="earnedDate"
|
||||
value={formData.earnedDate}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Geçerlilik Süresi (Opsiyonel)
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
name="expiryDate"
|
||||
value={formData.expiryDate}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Kazanma Nedeni
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="reason"
|
||||
value={formData.reason}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Örn: Q3 hedeflerini %120 başarıyla tamamladı"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Notlar
|
||||
</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
value={formData.notes}
|
||||
onChange={handleInputChange}
|
||||
rows={2}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Ek notlar..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 mt-3 pt-3 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<FaTimesCircle className="w-4 h-4" />
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-3 py-1 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2 shadow-md hover:shadow-lg"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Rozet Ata
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeAssignmentModal;
|
||||
|
|
@ -1,426 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
FaPlus,
|
||||
FaEdit,
|
||||
FaSave,
|
||||
FaTimesCircle,
|
||||
FaAward,
|
||||
FaPalette,
|
||||
FaInfoCircle,
|
||||
} from "react-icons/fa";
|
||||
import { HrBadge, BadgeCategoryEnum, BadgeRarityEnum } from "../../../types/hr";
|
||||
import { iconOptions } from "../../../utils/erp";
|
||||
|
||||
interface BadgeEditModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
badge?: HrBadge | null;
|
||||
onSubmit: (badgeData: HrBadge) => void;
|
||||
mode: "create" | "edit";
|
||||
}
|
||||
|
||||
const BadgeEditModal: React.FC<BadgeEditModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
badge,
|
||||
onSubmit,
|
||||
mode,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<HrBadge>({
|
||||
id: "",
|
||||
code: "",
|
||||
name: "",
|
||||
description: "",
|
||||
icon: "⭐",
|
||||
color: "#FFD700",
|
||||
backgroundColor: "#FFF8DC",
|
||||
category: BadgeCategoryEnum.Performance,
|
||||
criteria: "",
|
||||
points: 10,
|
||||
rarity: BadgeRarityEnum.Common,
|
||||
isActive: true,
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
});
|
||||
|
||||
const generateBadgeCode = () => {
|
||||
const timestamp = Date.now().toString().slice(-6);
|
||||
return `BADGE-${timestamp}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "edit" && badge) {
|
||||
setFormData({
|
||||
id: badge.id,
|
||||
code: badge.code,
|
||||
name: badge.name,
|
||||
description: badge.description,
|
||||
icon: badge.icon,
|
||||
color: badge.color,
|
||||
backgroundColor: badge.backgroundColor,
|
||||
category: badge.category,
|
||||
criteria: badge.criteria,
|
||||
points: badge.points,
|
||||
rarity: badge.rarity,
|
||||
isActive: badge.isActive,
|
||||
creationTime: badge.creationTime,
|
||||
lastModificationTime: badge.lastModificationTime,
|
||||
});
|
||||
} else {
|
||||
// Reset form for create mode
|
||||
setFormData({
|
||||
id: "",
|
||||
code: generateBadgeCode(),
|
||||
name: "",
|
||||
description: "",
|
||||
icon: "⭐",
|
||||
color: "#FFD700",
|
||||
backgroundColor: "#FFF8DC",
|
||||
category: BadgeCategoryEnum.Performance,
|
||||
criteria: "",
|
||||
points: 10,
|
||||
rarity: BadgeRarityEnum.Common,
|
||||
isActive: true,
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
});
|
||||
}
|
||||
}, [mode, badge, isOpen]);
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<
|
||||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
||||
>
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
|
||||
if (type === "checkbox") {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: checked,
|
||||
}));
|
||||
} else if (name === "points") {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: parseInt(value) || 0,
|
||||
}));
|
||||
} else {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit(formData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-t-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
{mode === "edit" ? (
|
||||
<FaEdit className="w-5 h-5 text-white" />
|
||||
) : (
|
||||
<FaPlus className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{mode === "edit" ? "Rozet Düzenle" : "Yeni Rozet Oluştur"}
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
{mode === "edit"
|
||||
? "Mevcut rozet bilgilerini güncelleyin"
|
||||
: "Yeni bir başarı rozeti tanımlayın"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 p-1 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<FaTimesCircle className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="p-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Sol Kolon */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<h4 className="text-base font-semibold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<FaAward className="w-5 h-5 text-blue-600" />
|
||||
Temel Bilgiler
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Rozet Kodu <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="code"
|
||||
value={formData.code}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Rozet kodunu girin..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Rozet Adı <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Rozet adını girin..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Açıklama
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
rows={1}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Rozet açıklaması..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Kazanma Kriteri <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="criteria"
|
||||
value={formData.criteria}
|
||||
onChange={handleInputChange}
|
||||
rows={2}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="Bu rozeti kazanmak için gerekli şartlar..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sağ Kolon */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<h4 className="text-base font-semibold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<FaPalette className="w-5 h-5 text-purple-600" />
|
||||
Görsel ve Özellikler
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
İkon
|
||||
</label>
|
||||
<select
|
||||
name="icon"
|
||||
value={formData.icon}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
>
|
||||
{iconOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.value} {option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Renk
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
name="color"
|
||||
value={formData.color}
|
||||
onChange={handleInputChange}
|
||||
className="w-full h-9 border border-gray-300 rounded-lg cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Arka Plan Rengi
|
||||
</label>
|
||||
<input
|
||||
type="color"
|
||||
name="backgroundColor"
|
||||
value={formData.backgroundColor}
|
||||
onChange={handleInputChange}
|
||||
className="w-full h-9 border border-gray-300 rounded-lg cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Kategori
|
||||
</label>
|
||||
<select
|
||||
name="category"
|
||||
value={formData.category}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
>
|
||||
{Object.values(BadgeCategoryEnum).map((category) => (
|
||||
<option key={category} value={category}>
|
||||
{category === BadgeCategoryEnum.Performance &&
|
||||
"Performans"}
|
||||
{category === BadgeCategoryEnum.Leadership &&
|
||||
"Liderlik"}
|
||||
{category === BadgeCategoryEnum.Innovation &&
|
||||
"İnovasyon"}
|
||||
{category === BadgeCategoryEnum.Teamwork &&
|
||||
"Takım Çalışması"}
|
||||
{category === BadgeCategoryEnum.Customer &&
|
||||
"Müşteri"}
|
||||
{category === BadgeCategoryEnum.Safety &&
|
||||
"Güvenlik"}
|
||||
{category === BadgeCategoryEnum.Attendance &&
|
||||
"Devam"}
|
||||
{category === BadgeCategoryEnum.Training &&
|
||||
"Eğitim"}
|
||||
{category === BadgeCategoryEnum.Project && "Proje"}
|
||||
{category === BadgeCategoryEnum.Special && "Özel"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Puan
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="points"
|
||||
value={formData.points}
|
||||
onChange={handleInputChange}
|
||||
min="1"
|
||||
max="1000"
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
placeholder="10"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Nadirlık
|
||||
</label>
|
||||
<select
|
||||
name="rarity"
|
||||
value={formData.rarity}
|
||||
onChange={handleInputChange}
|
||||
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
||||
>
|
||||
{Object.values(BadgeRarityEnum).map((rarity) => (
|
||||
<option key={rarity} value={rarity}>
|
||||
{rarity === BadgeRarityEnum.Common && "Yaygın"}
|
||||
{rarity === BadgeRarityEnum.Uncommon && "Nadir"}
|
||||
{rarity === BadgeRarityEnum.Rare && "Çok Nadir"}
|
||||
{rarity === BadgeRarityEnum.Epic && "Efsanevi"}
|
||||
{rarity === BadgeRarityEnum.Legendary && "Efsane"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={handleInputChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 block text-sm text-gray-900">
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<h4 className="text-base font-semibold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<FaInfoCircle className="w-5 h-5 text-green-600" />
|
||||
Önizleme
|
||||
</h4>
|
||||
<div className="flex items-center gap-3 p-3 bg-white rounded-lg border">
|
||||
<div
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center text-xl"
|
||||
style={{
|
||||
backgroundColor: formData.backgroundColor,
|
||||
border: `2px solid ${formData.color}`,
|
||||
}}
|
||||
>
|
||||
{formData.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{formData.name || "Rozet Adı"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{formData.points} puan • {formData.category}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 mt-4 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1.5 text-sm text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<FaTimesCircle className="w-4 h-4" />
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors flex items-center gap-2 shadow-md hover:shadow-lg"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
{mode === "edit" ? "Güncelle" : "Oluştur"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default BadgeEditModal;
|
||||
|
|
@ -1,446 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { FaAward, FaPlus, FaEdit, FaTrash, FaUsers } from 'react-icons/fa'
|
||||
import { HrBadge, HrEmployeeBadge } from '../../../types/hr'
|
||||
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { mockBadges } from '../../../mocks/mockBadges'
|
||||
import BadgeEditModal from './BadgeEditModal'
|
||||
import BadgeAssignmentModal, { BadgeAssignmentFormData } from './BadgeAssignmentModal'
|
||||
import Widget from '../../../components/common/Widget'
|
||||
import { getIconComponent } from '../../../utils/erp'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const BadgeManagement: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<'badges' | 'assignments'>('badges')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [searchAssignTerm, setSearchAssignTerm] = useState('')
|
||||
|
||||
// Modal states
|
||||
const [isBadgeModalOpen, setIsBadgeModalOpen] = useState(false)
|
||||
const [isAssignmentModalOpen, setIsAssignmentModalOpen] = useState(false)
|
||||
const [editingBadge, setEditingBadge] = useState<HrBadge | null>(null)
|
||||
|
||||
// Mock data - In real app, this would come from API
|
||||
const [badges, setBadges] = useState(mockBadges)
|
||||
const [employeeBadges, setEmployeeBadges] = useState<HrEmployeeBadge[]>([])
|
||||
|
||||
// Handlers
|
||||
const handleAddBadge = () => {
|
||||
setEditingBadge(null)
|
||||
setIsBadgeModalOpen(true)
|
||||
}
|
||||
|
||||
const handleEditBadge = (badge: HrBadge) => {
|
||||
setEditingBadge(badge)
|
||||
setIsBadgeModalOpen(true)
|
||||
}
|
||||
|
||||
const handleDeleteBadge = (id: string) => {
|
||||
if (window.confirm('Bu rozeti silmek istediğinizden emin misiniz?')) {
|
||||
setBadges(badges.filter((badge) => badge.id !== id))
|
||||
alert('Rozet başarıyla silindi!')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRevokeBadge = (employeeBadgeId: string) => {
|
||||
if (window.confirm('Bu rozet atamasını iptal etmek istediğinizden emin misiniz?')) {
|
||||
setEmployeeBadges((prev) =>
|
||||
prev.map((eb) => (eb.id === employeeBadgeId ? { ...eb, isActive: false } : eb)),
|
||||
)
|
||||
alert('Rozet ataması iptal edildi!')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBadgeSubmit = (badgeData: HrBadge) => {
|
||||
if (editingBadge) {
|
||||
// Update existing badge
|
||||
setBadges((prev) =>
|
||||
prev.map((badge) =>
|
||||
badge.id === editingBadge.id
|
||||
? {
|
||||
...badge,
|
||||
...badgeData,
|
||||
lastModificationTime: new Date(),
|
||||
}
|
||||
: badge,
|
||||
),
|
||||
)
|
||||
alert('Rozet başarıyla güncellendi!')
|
||||
} else {
|
||||
// Create new badge
|
||||
const newBadge: HrBadge = {
|
||||
...badgeData,
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
}
|
||||
setBadges((prev) => [...prev, newBadge])
|
||||
alert('Yeni rozet başarıyla oluşturuldu!')
|
||||
}
|
||||
setIsBadgeModalOpen(false)
|
||||
setEditingBadge(null)
|
||||
}
|
||||
|
||||
const handleAssignmentSubmit = (assignmentData: BadgeAssignmentFormData) => {
|
||||
const employee = mockEmployees.find((emp) => emp.id === assignmentData.employeeId)
|
||||
const badge = badges.find((b) => b.id === assignmentData.badgeId)
|
||||
|
||||
if (employee && badge) {
|
||||
const newAssignment: HrEmployeeBadge = {
|
||||
id: `assignment-${Date.now()}`,
|
||||
employeeId: assignmentData.employeeId,
|
||||
employee: employee,
|
||||
badgeId: assignmentData.badgeId,
|
||||
badge: badge,
|
||||
earnedDate: new Date(assignmentData.earnedDate),
|
||||
expiryDate: assignmentData.expiryDate ? new Date(assignmentData.expiryDate) : undefined,
|
||||
reason: assignmentData.reason,
|
||||
notes: assignmentData.notes,
|
||||
isActive: true,
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
}
|
||||
|
||||
setEmployeeBadges((prev) => [...prev, newAssignment])
|
||||
alert('Rozet başarıyla atandı!')
|
||||
}
|
||||
setIsAssignmentModalOpen(false)
|
||||
}
|
||||
|
||||
const openAssignmentModal = () => {
|
||||
setIsAssignmentModalOpen(true)
|
||||
}
|
||||
|
||||
const filteredBadges = badges.filter(
|
||||
(badge) =>
|
||||
badge.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
badge.description.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
|
||||
const filteredEmployeeBadges = employeeBadges.filter((eb) =>
|
||||
eb.employee?.fullName.toLowerCase().includes(searchAssignTerm.toLowerCase()),
|
||||
)
|
||||
|
||||
const badgeColumns: Column<HrBadge>[] = [
|
||||
{
|
||||
key: 'icon',
|
||||
header: 'Simge',
|
||||
render: (badge: HrBadge) => {
|
||||
const IconComponent = getIconComponent(badge.icon)
|
||||
return (
|
||||
<div
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center`}
|
||||
style={{ backgroundColor: badge.color }}
|
||||
>
|
||||
<IconComponent className="w-3.5 h-3.5 text-white" />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Rozet Adı',
|
||||
sortable: true,
|
||||
render: (badge: HrBadge) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{badge.name}</div>
|
||||
<div className="text-sm text-gray-500 truncate max-w-xs">{badge.description}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'criteria',
|
||||
header: 'Kazanma Kriteri',
|
||||
render: (badge: HrBadge) => (
|
||||
<div className="text-sm text-gray-600 max-w-xs truncate">{badge.criteria}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'assignedCount',
|
||||
header: 'Atanan Sayısı',
|
||||
render: (badge: HrBadge) => {
|
||||
const count = employeeBadges.filter((eb) => eb.badgeId === badge.id && eb.isActive).length
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaUsers className="w-4 h-4 text-gray-500" />
|
||||
<span>{count}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Durum',
|
||||
render: (badge: HrBadge) => (
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
||||
badge.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{badge.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'İşlemler',
|
||||
render: (badge: HrBadge) => (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEditBadge(badge)}
|
||||
className="p-0.5 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteBadge(badge.id)}
|
||||
className="p-0.5 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const assignmentColumns: Column<HrEmployeeBadge>[] = [
|
||||
{
|
||||
key: 'employee',
|
||||
header: 'Personel',
|
||||
render: (assignment: HrEmployeeBadge) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{assignment.employee?.fullName}</div>
|
||||
<div className="text-sm text-gray-500">{assignment.employee?.code}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'badge',
|
||||
header: 'Rozet',
|
||||
render: (assignment: HrEmployeeBadge) => {
|
||||
const IconComponent = getIconComponent(assignment.badge?.icon || 'award')
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: assignment.badge?.color }}
|
||||
>
|
||||
<IconComponent className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span>{assignment.badge?.name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'earnedDate',
|
||||
header: 'Kazanıldığı Tarih',
|
||||
render: (assignment: HrEmployeeBadge) =>
|
||||
new Date(assignment.earnedDate).toLocaleDateString('tr-TR'),
|
||||
},
|
||||
{
|
||||
key: 'expiryDate',
|
||||
header: 'Geçerlilik Süresi',
|
||||
render: (assignment: HrEmployeeBadge) =>
|
||||
assignment.expiryDate
|
||||
? new Date(assignment.expiryDate).toLocaleDateString('tr-TR')
|
||||
: 'Süresiz',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Durum',
|
||||
render: (assignment: HrEmployeeBadge) => {
|
||||
const isExpired = assignment.expiryDate && new Date(assignment.expiryDate) < new Date()
|
||||
return (
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
||||
!assignment.isActive
|
||||
? 'bg-red-100 text-red-800'
|
||||
: isExpired
|
||||
? 'bg-yellow-100 text-yellow-800'
|
||||
: 'bg-green-100 text-green-800'
|
||||
}`}
|
||||
>
|
||||
{!assignment.isActive ? 'İptal Edildi' : isExpired ? 'Süresi Dolmuş' : 'Aktif'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'İşlemler',
|
||||
render: (assignment: HrEmployeeBadge) => (
|
||||
<div className="flex gap-2">
|
||||
{assignment.isActive && (
|
||||
<button
|
||||
onClick={() => handleRevokeBadge(assignment.id)}
|
||||
className="p-0.5 text-red-600 hover:bg-red-50 rounded"
|
||||
title="İptal Et"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Rozet Yönetimi</h2>
|
||||
<p className="text-gray-600">
|
||||
Personel başarı rozetleri ve ödüllendirme sistemi
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<Widget title="Toplam Rozet" value={badges.length} color="blue" icon="FaAward" />
|
||||
|
||||
<Widget
|
||||
title="Aktif Rozet"
|
||||
value={badges.filter((b) => b.isActive).length}
|
||||
color="yellow"
|
||||
icon="FaTrophy"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Toplam Atama"
|
||||
value={filteredEmployeeBadges.length}
|
||||
color="green"
|
||||
icon="FaUsers"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Rozetli Personel"
|
||||
value={new Set(filteredEmployeeBadges.map((ab) => ab.employeeId)).size}
|
||||
color="purple"
|
||||
icon="FaStar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8">
|
||||
<button
|
||||
onClick={() => setActiveTab('badges')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'badges'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Rozetler
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('assignments')}
|
||||
className={`py-2 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab === 'assignments'
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
Rozet Atamaları
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'badges' && (
|
||||
<div className="space-y-3 pt-2">
|
||||
{/* Search */}
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rozet adı veya açıklama ara..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleAddBadge}
|
||||
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" />
|
||||
<span className="whitespace-nowrap">Yeni Rozet</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Badges Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<DataTable data={filteredBadges} columns={badgeColumns} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'assignments' && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="bg-white p-3 rounded-lg shadow-sm border">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rozet adı veya açıklama ara..."
|
||||
value={searchAssignTerm}
|
||||
onChange={(e) => setSearchAssignTerm(e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<button
|
||||
className="px-3 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors"
|
||||
onClick={openAssignmentModal}
|
||||
>
|
||||
<span className="whitespace-nowrap">Rozet Ata</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assignments Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<DataTable data={filteredEmployeeBadges} columns={assignmentColumns} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(activeTab === 'badges' ? filteredBadges : filteredEmployeeBadges).length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<FaAward className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
{activeTab === 'badges' ? 'Rozet bulunamadı' : 'Rozet ataması bulunamadı'}
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
{activeTab === 'badges'
|
||||
? 'Henüz rozet tanımlanmamış.'
|
||||
: 'Henüz rozet ataması yapılmamış.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badge Edit Modal */}
|
||||
<BadgeEditModal
|
||||
isOpen={isBadgeModalOpen}
|
||||
onClose={() => setIsBadgeModalOpen(false)}
|
||||
badge={editingBadge}
|
||||
onSubmit={handleBadgeSubmit}
|
||||
mode={editingBadge ? 'edit' : 'create'}
|
||||
/>
|
||||
|
||||
{/* Badge Assignment Modal */}
|
||||
<BadgeAssignmentModal
|
||||
isOpen={isAssignmentModalOpen}
|
||||
onClose={() => setIsAssignmentModalOpen(false)}
|
||||
onSubmit={handleAssignmentSubmit}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default BadgeManagement
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { FaSave, FaTimes } from 'react-icons/fa'
|
||||
import { HrCostCenter, CostCenterType } from '../../../types/hr'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { mockCostCenters } from '../../../mocks/mockCostCenters'
|
||||
import { getCostCenterTypeText } from '../../../utils/erp'
|
||||
import { mockCurrencies } from '@/mocks/mockCurrencies'
|
||||
|
||||
interface CostCenterFormModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (costCenter: Partial<HrCostCenter>) => void
|
||||
costCenter?: HrCostCenter
|
||||
title: string
|
||||
}
|
||||
|
||||
const CostCenterFormModal: React.FC<CostCenterFormModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
costCenter,
|
||||
title,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
costCenterCode: '',
|
||||
name: '',
|
||||
description: '',
|
||||
costCenterType: CostCenterType.Standard,
|
||||
responsibleEmployeeId: '',
|
||||
parentCostCenterId: '',
|
||||
budgetedAmount: 0,
|
||||
actualAmount: 0,
|
||||
currency: 'TRY',
|
||||
fiscalYear: '2025',
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (costCenter) {
|
||||
setFormData({
|
||||
costCenterCode: costCenter.code,
|
||||
name: costCenter.name,
|
||||
description: costCenter.description || '',
|
||||
costCenterType: costCenter.costCenterType,
|
||||
responsibleEmployeeId: costCenter.responsibleEmployeeId || '',
|
||||
parentCostCenterId: costCenter.parentCostCenterId || '',
|
||||
budgetedAmount: costCenter.budgetedAmount,
|
||||
actualAmount: costCenter.actualAmount,
|
||||
currency: costCenter.currency,
|
||||
fiscalYear: costCenter.fiscalYear,
|
||||
isActive: costCenter.isActive,
|
||||
})
|
||||
} else {
|
||||
setFormData({
|
||||
costCenterCode: '',
|
||||
name: '',
|
||||
description: '',
|
||||
costCenterType: CostCenterType.Standard,
|
||||
responsibleEmployeeId: '',
|
||||
parentCostCenterId: '',
|
||||
budgetedAmount: 0,
|
||||
actualAmount: 0,
|
||||
currency: 'TRY',
|
||||
fiscalYear: '2025',
|
||||
isActive: true,
|
||||
})
|
||||
}
|
||||
}, [costCenter])
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
onSave(formData)
|
||||
}
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-3 w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-base font-bold text-gray-900">{title}</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Maliyet Merkezi Kodu *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.costCenterCode}
|
||||
onChange={(e) => setFormData({ ...formData, costCenterCode: e.target.value })}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Maliyet Merkezi Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Açıklama</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Maliyet Merkezi Tipi
|
||||
</label>
|
||||
<select
|
||||
value={formData.costCenterType}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
costCenterType: e.target.value as CostCenterType,
|
||||
})
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{Object.values(CostCenterType).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getCostCenterTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Sorumlu Personel
|
||||
</label>
|
||||
<select
|
||||
value={formData.responsibleEmployeeId}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
responsibleEmployeeId: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Seçiniz</option>
|
||||
{mockEmployees.map((employee) => (
|
||||
<option key={employee.id} value={employee.id}>
|
||||
{employee.fullName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Üst Maliyet Merkezi
|
||||
</label>
|
||||
<select
|
||||
value={formData.parentCostCenterId}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
parentCostCenterId: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Ana Maliyet Merkezi</option>
|
||||
{mockCostCenters
|
||||
.filter((cc) => cc.id !== costCenter?.id)
|
||||
.map((costCenter) => (
|
||||
<option key={costCenter.id} value={costCenter.id}>
|
||||
{costCenter.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Mali Yıl</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.fiscalYear}
|
||||
onChange={(e) => setFormData({ ...formData, fiscalYear: e.target.value })}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Bütçe Tutarı</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.budgetedAmount}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
budgetedAmount: parseFloat(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Gerçekleşen Tutar
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.actualAmount}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
actualAmount: parseFloat(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Para Birimi</label>
|
||||
<select
|
||||
value={formData.currency}
|
||||
onChange={(e) => setFormData({ ...formData, currency: e.target.value })}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{mockCurrencies.map((currency) => (
|
||||
<option key={currency.value} value={currency.value}>
|
||||
{currency.value} - {currency.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label className="ml-2 block text-sm text-gray-900">Aktif</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default CostCenterFormModal
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import {
|
||||
FaPlus,
|
||||
FaEdit,
|
||||
FaTrash,
|
||||
FaChartPie,
|
||||
FaDollarSign,
|
||||
FaPercentage,
|
||||
FaEye,
|
||||
FaList,
|
||||
FaTh,
|
||||
} from 'react-icons/fa'
|
||||
import { HrCostCenter, CostCenterType } from '../../../types/hr'
|
||||
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||
import { mockCostCenters } from '../../../mocks/mockCostCenters'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import CostCenterFormModal from './CostCenterFormModal'
|
||||
import CostCenterViewModal from './CostCenterViewModal'
|
||||
import Widget from '../../../components/common/Widget'
|
||||
import { getCostCenterTypeColor, getCostCenterTypeText } from '../../../utils/erp'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const CostCenterManagement: React.FC = () => {
|
||||
const [costCenters, setCostCenters] = useState<HrCostCenter[]>(mockCostCenters)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedType, setSelectedType] = useState<string>('all')
|
||||
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
|
||||
const [isFormModalOpen, setIsFormModalOpen] = useState(false)
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
||||
const [selectedCostCenter, setSelectedCostCenter] = useState<HrCostCenter | null>(null)
|
||||
const [editingCostCenter, setEditingCostCenter] = useState<HrCostCenter | undefined>(undefined)
|
||||
|
||||
// Cost center bağlantılarını kur
|
||||
mockCostCenters.forEach((cc) => {
|
||||
if (cc.responsibleEmployeeId) {
|
||||
cc.responsibleEmployee = mockEmployees.find((emp) => emp.id === cc.responsibleEmployeeId)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingCostCenter(undefined)
|
||||
setIsFormModalOpen(true)
|
||||
}
|
||||
|
||||
const handleEdit = (costCenter: HrCostCenter) => {
|
||||
setEditingCostCenter(costCenter)
|
||||
setIsFormModalOpen(true)
|
||||
}
|
||||
|
||||
const handleView = (costCenter: HrCostCenter) => {
|
||||
setSelectedCostCenter(costCenter)
|
||||
setIsViewModalOpen(true)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm('Bu maliyet merkezini silmek istediğinizden emin misiniz?')) {
|
||||
setCostCenters(costCenters.filter((cc) => cc.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = (costCenterData: Partial<HrCostCenter>) => {
|
||||
if (editingCostCenter) {
|
||||
// Edit existing
|
||||
setCostCenters(
|
||||
costCenters.map((cc) =>
|
||||
cc.id === editingCostCenter.id
|
||||
? {
|
||||
...cc,
|
||||
...costCenterData,
|
||||
lastModificationTime: new Date(),
|
||||
}
|
||||
: cc,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// Add new
|
||||
const newCostCenter: HrCostCenter = {
|
||||
id: `cc-${Date.now()}`,
|
||||
...costCenterData,
|
||||
subCostCenters: [],
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
} as HrCostCenter
|
||||
setCostCenters([...costCenters, newCostCenter])
|
||||
}
|
||||
setIsFormModalOpen(false)
|
||||
setEditingCostCenter(undefined)
|
||||
}
|
||||
|
||||
const filteredCostCenters = costCenters.filter((costCenter) => {
|
||||
if (
|
||||
searchTerm &&
|
||||
!costCenter.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||
!costCenter.code.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (selectedType !== 'all' && costCenter.costCenterType !== selectedType) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const getVariancePercentage = (budgeted: number, actual: number): number => {
|
||||
if (budgeted === 0) return 0
|
||||
return ((actual - budgeted) / budgeted) * 100
|
||||
}
|
||||
|
||||
const columns: Column<HrCostCenter>[] = [
|
||||
{
|
||||
key: 'code',
|
||||
header: 'Maliyet Merkezi Kodu',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Maliyet Merkezi Adı',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'costCenterType',
|
||||
header: 'Tip',
|
||||
render: (costCenter: HrCostCenter) => (
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${getCostCenterTypeColor(
|
||||
costCenter.costCenterType,
|
||||
)}`}
|
||||
>
|
||||
{getCostCenterTypeText(costCenter.costCenterType)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'responsibleEmployee',
|
||||
header: 'Sorumlu',
|
||||
render: (costCenter: HrCostCenter) => costCenter.responsibleEmployee?.fullName || '-',
|
||||
},
|
||||
{
|
||||
key: 'budgetedAmount',
|
||||
header: 'Bütçe',
|
||||
render: (costCenter: HrCostCenter) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
||||
<span>₺{costCenter.budgetedAmount.toLocaleString()}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actualAmount',
|
||||
header: 'Gerçekleşen',
|
||||
render: (costCenter: HrCostCenter) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
||||
<span>₺{costCenter.actualAmount.toLocaleString()}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'variance',
|
||||
header: 'Fark (%)',
|
||||
render: (costCenter: HrCostCenter) => {
|
||||
const variance = getVariancePercentage(costCenter.budgetedAmount, costCenter.actualAmount)
|
||||
const isPositive = variance > 0
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaPercentage className="w-4 h-4 text-gray-500" />
|
||||
<span className={`font-medium ${isPositive ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{isPositive ? '+' : ''}
|
||||
{variance.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Durum',
|
||||
render: (costCenter: HrCostCenter) => (
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
||||
costCenter.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{costCenter.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'İşlemler',
|
||||
render: (costCenter: HrCostCenter) => (
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleView(costCenter)}
|
||||
className="p-0.5 text-green-600 hover:bg-green-50 rounded"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(costCenter)}
|
||||
className="p-0.5 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(costCenter.id)}
|
||||
className="p-0.5 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const totalBudget = costCenters.reduce((sum, cc) => sum + cc.budgetedAmount, 0)
|
||||
const totalActual = costCenters.reduce((sum, cc) => sum + cc.actualAmount, 0)
|
||||
const totalVariance = getVariancePercentage(totalBudget, totalActual)
|
||||
|
||||
const renderCards = () => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{filteredCostCenters.map((costCenter) => {
|
||||
const variance = getVariancePercentage(costCenter.budgetedAmount, costCenter.actualAmount)
|
||||
const isPositive = variance > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
key={costCenter.id}
|
||||
className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">{costCenter.name}</h3>
|
||||
<p className="text-gray-600">{costCenter.code}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${getCostCenterTypeColor(
|
||||
costCenter.costCenterType,
|
||||
)}`}
|
||||
>
|
||||
{getCostCenterTypeText(costCenter.costCenterType)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{costCenter.description && (
|
||||
<p className="text-sm text-gray-600 mb-4 line-clamp-2">{costCenter.description}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">Bütçe:</span>
|
||||
<span className="font-medium">₺{costCenter.budgetedAmount.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">Gerçekleşen:</span>
|
||||
<span className="font-medium">₺{costCenter.actualAmount.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-600">Fark:</span>
|
||||
<span className={`font-medium ${isPositive ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{isPositive ? '+' : ''}
|
||||
{variance.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{costCenter.responsibleEmployee && (
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-sm text-gray-600">Sorumlu:</span>
|
||||
<span className="text-sm font-medium">
|
||||
{costCenter.responsibleEmployee.fullName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<span
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full ${
|
||||
costCenter.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{costCenter.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => handleView(costCenter)}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(costCenter)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(costCenter.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Maliyet Merkezi Yönetimi</h2>
|
||||
<p className="text-gray-600">
|
||||
Maliyet merkezlerini ve bütçe takibini yönetin
|
||||
</p>
|
||||
</div>
|
||||
<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 Maliyet Merkezi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Widget
|
||||
title="Toplam Maliyet Merkezi"
|
||||
value={costCenters.length}
|
||||
color="blue"
|
||||
icon="FaChartPie"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Toplam Bütçe"
|
||||
value={`₺${totalBudget.toLocaleString()}`}
|
||||
color="green"
|
||||
icon="FaDollarSign"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Toplam Gerçekleşen"
|
||||
value={`₺${totalActual.toLocaleString()}`}
|
||||
color="orange"
|
||||
icon="FaDollarSign"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Toplam Fark"
|
||||
value={`${totalVariance > 0 ? '+' : ''}${totalVariance.toFixed(1)}%`}
|
||||
color={totalVariance > 0 ? 'red' : 'green'}
|
||||
icon="FaPercentage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters and View Mode */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-stretch sm:items-center justify-between">
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-stretch sm:items-center flex-1">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Maliyet merkezi adı veya kodu ara..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(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>
|
||||
|
||||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[200px]"
|
||||
>
|
||||
<option value="all">Tüm Tipler</option>
|
||||
{Object.values(CostCenterType).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getCostCenterTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex items-center gap-2 bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-1.5 rounded-md text-sm transition-colors ${
|
||||
viewMode === 'list'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title="Liste Görünümü"
|
||||
>
|
||||
<FaList className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('cards')}
|
||||
className={`p-1.5 rounded-md text-sm transition-colors ${
|
||||
viewMode === 'cards'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title="Kart Görünümü"
|
||||
>
|
||||
<FaTh className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{viewMode === 'list' ? (
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<DataTable data={filteredCostCenters} columns={columns} />
|
||||
</div>
|
||||
) : (
|
||||
renderCards()
|
||||
)}
|
||||
|
||||
{filteredCostCenters.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<FaChartPie className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">Maliyet merkezi bulunamadı</h3>
|
||||
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<CostCenterFormModal
|
||||
isOpen={isFormModalOpen}
|
||||
onClose={() => {
|
||||
setIsFormModalOpen(false)
|
||||
setEditingCostCenter(undefined)
|
||||
}}
|
||||
onSave={handleSave}
|
||||
costCenter={editingCostCenter}
|
||||
title={editingCostCenter ? 'Maliyet Merkezi Düzenle' : 'Yeni Maliyet Merkezi'}
|
||||
/>
|
||||
|
||||
<CostCenterViewModal
|
||||
isOpen={isViewModalOpen}
|
||||
onClose={() => {
|
||||
setIsViewModalOpen(false)
|
||||
setSelectedCostCenter(null)
|
||||
}}
|
||||
costCenter={selectedCostCenter}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default CostCenterManagement
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
import React from "react";
|
||||
import {
|
||||
FaTimes,
|
||||
FaDollarSign,
|
||||
FaPercentage,
|
||||
FaUser,
|
||||
FaBuilding,
|
||||
} from "react-icons/fa";
|
||||
import { HrCostCenter } from "../../../types/hr";
|
||||
import {
|
||||
getCostCenterTypeColor,
|
||||
getCostCenterTypeText,
|
||||
} from "../../../utils/erp";
|
||||
|
||||
interface CostCenterViewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
costCenter: HrCostCenter | null;
|
||||
}
|
||||
|
||||
const CostCenterViewModal: React.FC<CostCenterViewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
costCenter,
|
||||
}) => {
|
||||
const getVariancePercentage = (budgeted: number, actual: number): number => {
|
||||
if (budgeted === 0) return 0;
|
||||
return ((actual - budgeted) / budgeted) * 100;
|
||||
};
|
||||
|
||||
const variance = costCenter
|
||||
? getVariancePercentage(costCenter.budgetedAmount, costCenter.actualAmount)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
isOpen &&
|
||||
costCenter && (
|
||||
<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-xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
Maliyet Merkezi Detayları
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Temel Bilgiler */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<FaBuilding className="w-5 h-5" />
|
||||
Temel Bilgiler
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Maliyet Merkezi Kodu
|
||||
</label>
|
||||
<p className="text-base font-semibold text-gray-900">
|
||||
{costCenter.code}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Maliyet Merkezi Adı
|
||||
</label>
|
||||
<p className="text-base font-semibold text-gray-900">
|
||||
{costCenter.name}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Tip
|
||||
</label>
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full ${getCostCenterTypeColor(
|
||||
costCenter.costCenterType
|
||||
)}`}
|
||||
>
|
||||
{getCostCenterTypeText(costCenter.costCenterType)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Mali Yıl
|
||||
</label>
|
||||
<p className="text-base font-semibold text-gray-900">
|
||||
{costCenter.fiscalYear}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{costCenter.description && (
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Açıklama
|
||||
</label>
|
||||
<p className="text-gray-900">{costCenter.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sorumlu Bilgileri */}
|
||||
{costCenter.responsibleEmployee && (
|
||||
<div className="bg-blue-50 rounded-lg p-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<FaUser className="w-5 h-5" />
|
||||
Sorumlu Personel
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Ad Soyad
|
||||
</label>
|
||||
<p className="text-base font-semibold text-gray-900">
|
||||
{costCenter.responsibleEmployee.fullName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
E-posta
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{costCenter.responsibleEmployee.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bütçe Bilgileri */}
|
||||
<div className="bg-green-50 rounded-lg p-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<FaDollarSign className="w-5 h-5" />
|
||||
Bütçe Bilgileri
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Bütçe Tutarı
|
||||
</label>
|
||||
<p className="text-lg font-bold text-green-600">
|
||||
{costCenter.currency}{" "}
|
||||
{costCenter.budgetedAmount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Gerçekleşen Tutar
|
||||
</label>
|
||||
<p className="text-lg font-bold text-blue-600">
|
||||
{costCenter.currency}{" "}
|
||||
{costCenter.actualAmount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Fark
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaPercentage className="w-4 h-4 text-gray-500" />
|
||||
<span
|
||||
className={`text-lg font-bold ${
|
||||
variance > 0 ? "text-red-600" : "text-green-600"
|
||||
}`}
|
||||
>
|
||||
{variance > 0 ? "+" : ""}
|
||||
{variance.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bütçe Görselleştirme */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-2">
|
||||
Bütçe Kullanım Oranı
|
||||
</label>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div
|
||||
className={`h-2.5 rounded-full ${
|
||||
variance > 0 ? "bg-red-500" : "bg-green-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(
|
||||
(costCenter.actualAmount / costCenter.budgetedAmount) *
|
||||
100,
|
||||
100
|
||||
)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{(
|
||||
(costCenter.actualAmount / costCenter.budgetedAmount) *
|
||||
100
|
||||
).toFixed(1)}
|
||||
% kullanıldı
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hiyerarşi Bilgileri */}
|
||||
{costCenter.parentCostCenter && (
|
||||
<div className="bg-purple-50 rounded-lg p-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Hiyerarşi
|
||||
</h3>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Üst Maliyet Merkezi
|
||||
</label>
|
||||
<p className="text-base font-semibold text-gray-900">
|
||||
{costCenter.parentCostCenter.name} (
|
||||
{costCenter.parentCostCenter.code})
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Durum Bilgisi */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Durum
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Mevcut Durum
|
||||
</label>
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full ${
|
||||
costCenter.isActive
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{costCenter.isActive ? "Aktif" : "Pasif"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-600">
|
||||
Son Güncelleme
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{new Date(
|
||||
costCenter.lastModificationTime
|
||||
).toLocaleDateString("tr-TR")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 mt-4 border-t">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1.5 bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default CostCenterViewModal;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,781 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { FaEye, FaEdit, FaPlus, FaTimes, FaSave, FaTrash, FaQuestionCircle } from 'react-icons/fa'
|
||||
import {
|
||||
HrEvaluation360Template,
|
||||
HrQuestionGroup,
|
||||
HrEvaluationQuestion,
|
||||
QuestionTypeEnum,
|
||||
HrQuestionOption,
|
||||
AssessorTypeEnum,
|
||||
getQuestionTypeText,
|
||||
} from '../../../types/hr'
|
||||
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||
import { mockEvaluation360Templates } from '../../../mocks/mockEvaluation360Templates'
|
||||
import Widget from '../../../components/common/Widget'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const Degree360Templates: React.FC = () => {
|
||||
const [templateSearchTerm, setTemplateSearchTerm] = useState('')
|
||||
|
||||
// Modal states
|
||||
const [showTemplateModal, setShowTemplateModal] = useState(false)
|
||||
const [showQuestionModal, setShowQuestionModal] = useState(false)
|
||||
const [showQuestionItemModal, setShowQuestionItemModal] = useState(false)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<HrEvaluation360Template | null>(null)
|
||||
const [selectedQuestionGroup, setSelectedQuestionGroup] = useState<HrQuestionGroup | null>(null)
|
||||
const [selectedQuestion, setSelectedQuestion] = useState<HrEvaluationQuestion | null>(null)
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
|
||||
// Form states
|
||||
const [templateForm, setTemplateForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
isActive: true,
|
||||
assessorTypes: [] as AssessorTypeEnum[],
|
||||
})
|
||||
|
||||
const [questionGroupForm, setQuestionGroupForm] = useState({
|
||||
groupName: '',
|
||||
description: '',
|
||||
weight: 0,
|
||||
order: 0,
|
||||
})
|
||||
|
||||
const [questionForm, setQuestionForm] = useState({
|
||||
questionText: '',
|
||||
questionType: QuestionTypeEnum.Rating,
|
||||
isRequired: true,
|
||||
order: 0,
|
||||
weight: 0,
|
||||
options: [] as HrQuestionOption[],
|
||||
})
|
||||
|
||||
// Filter templates
|
||||
const filteredTemplates = mockEvaluation360Templates.filter((template) => {
|
||||
const searchLower = templateSearchTerm.toLowerCase()
|
||||
return (
|
||||
template.name.toLowerCase().includes(searchLower) ||
|
||||
template.description.toLowerCase().includes(searchLower)
|
||||
)
|
||||
})
|
||||
|
||||
// Statistics
|
||||
const totalTemplates = mockEvaluation360Templates.length
|
||||
const activeTemplates = mockEvaluation360Templates.filter((t) => t.isActive).length
|
||||
|
||||
// Event handlers
|
||||
const handleNewTemplate = () => {
|
||||
setTemplateForm({
|
||||
name: '',
|
||||
description: '',
|
||||
isActive: true,
|
||||
assessorTypes: [],
|
||||
})
|
||||
setSelectedTemplate(null)
|
||||
setIsEditMode(false)
|
||||
setShowTemplateModal(true)
|
||||
}
|
||||
|
||||
const handleEditTemplate = (template: HrEvaluation360Template) => {
|
||||
setTemplateForm({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
isActive: template.isActive,
|
||||
assessorTypes: template.assessorTypes || [],
|
||||
})
|
||||
setSelectedTemplate(template)
|
||||
setIsEditMode(true)
|
||||
setShowTemplateModal(true)
|
||||
}
|
||||
|
||||
const handleViewTemplate = (template: HrEvaluation360Template) => {
|
||||
setTemplateForm({
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
isActive: template.isActive,
|
||||
assessorTypes: template.assessorTypes || [],
|
||||
})
|
||||
setSelectedTemplate(template)
|
||||
setShowTemplateModal(true)
|
||||
setIsEditMode(false)
|
||||
}
|
||||
|
||||
const handleSaveTemplate = () => {
|
||||
console.log('Saving template:', templateForm)
|
||||
setShowTemplateModal(false)
|
||||
}
|
||||
|
||||
const handleDeleteTemplate = (template: HrEvaluation360Template) => {
|
||||
if (confirm(`"${template.name}" şablonunu silmek istediğinizden emin misiniz?`)) {
|
||||
console.log('Deleting template:', template.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddQuestionGroup = () => {
|
||||
setQuestionGroupForm({
|
||||
groupName: '',
|
||||
description: '',
|
||||
weight: 0,
|
||||
order: 0,
|
||||
})
|
||||
setSelectedQuestionGroup(null)
|
||||
setShowQuestionModal(true)
|
||||
}
|
||||
|
||||
const handleEditQuestionGroup = (group: HrQuestionGroup) => {
|
||||
setQuestionGroupForm({
|
||||
groupName: group.groupName,
|
||||
description: group.description || '',
|
||||
weight: group.weight,
|
||||
order: group.order,
|
||||
})
|
||||
setSelectedQuestionGroup(group)
|
||||
setShowQuestionModal(true)
|
||||
}
|
||||
|
||||
const handleSaveQuestionGroup = () => {
|
||||
console.log('Saving question group:', questionGroupForm)
|
||||
setShowQuestionModal(false)
|
||||
}
|
||||
|
||||
const handleAddQuestion = (group: HrQuestionGroup) => {
|
||||
setQuestionForm({
|
||||
questionText: '',
|
||||
questionType: QuestionTypeEnum.Rating,
|
||||
isRequired: true,
|
||||
order: 0,
|
||||
weight: 0,
|
||||
options: [],
|
||||
})
|
||||
setSelectedQuestion(null)
|
||||
setSelectedQuestionGroup(group)
|
||||
setShowQuestionItemModal(true)
|
||||
}
|
||||
|
||||
const handleEditQuestion = (question: HrEvaluationQuestion, group: HrQuestionGroup) => {
|
||||
setQuestionForm({
|
||||
questionText: question.questionText,
|
||||
questionType: question.questionType,
|
||||
isRequired: question.isRequired,
|
||||
order: question.order,
|
||||
weight: question.weight,
|
||||
options: question.options || [],
|
||||
})
|
||||
setSelectedQuestion(question)
|
||||
setSelectedQuestionGroup(group)
|
||||
setShowQuestionItemModal(true)
|
||||
}
|
||||
|
||||
const handleSaveQuestion = () => {
|
||||
console.log('Saving question:', questionForm)
|
||||
setShowQuestionItemModal(false)
|
||||
}
|
||||
|
||||
// Table columns
|
||||
const templateColumns: Column<HrEvaluation360Template>[] = [
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Şablon Adı',
|
||||
sortable: true,
|
||||
render: (template) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{template.name}</div>
|
||||
<div className="text-sm text-gray-500">{template.description}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'questionGroups',
|
||||
header: 'Soru Grupları',
|
||||
render: (template) => (
|
||||
<span className="text-sm text-gray-600">{template.questionGroups?.length || 0} grup</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'assessorTypes',
|
||||
header: 'Değerlendirecekler',
|
||||
render: (template) => {
|
||||
const assessorLabels = {
|
||||
[AssessorTypeEnum.Self]: 'Kendi',
|
||||
[AssessorTypeEnum.Manager]: 'Yönetici',
|
||||
[AssessorTypeEnum.Peer]: 'Meslektaş',
|
||||
[AssessorTypeEnum.Subordinate]: 'Ast',
|
||||
[AssessorTypeEnum.Customer]: 'Müşteri',
|
||||
[AssessorTypeEnum.OtherDepartment]: 'Diğer Departman',
|
||||
[AssessorTypeEnum.HRUpperManagement]: 'İK/Üst Yönetim',
|
||||
[AssessorTypeEnum.External]: 'Dış Paydaş',
|
||||
}
|
||||
|
||||
const assessorTypes = template.assessorTypes || []
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{assessorTypes.map((type) => (
|
||||
<span
|
||||
key={type}
|
||||
className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{assessorLabels[type] || type}
|
||||
</span>
|
||||
))}
|
||||
{assessorTypes.length === 0 && (
|
||||
<span className="text-sm text-gray-400">Belirtilmemiş</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
header: 'Durum',
|
||||
render: (template) => (
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
template.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{template.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'İşlemler',
|
||||
render: (template) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleViewTemplate(template)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditTemplate(template)}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTemplate(template)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">360° Şablonlar</h2>
|
||||
<p className="text-gray-600">360 derece değerlendirme şablonlarını yönetin</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleNewTemplate}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-purple-600 text-white rounded-md hover:bg-purple-700 ml-4 text-sm"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
Yeni Şablon
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Widget
|
||||
title="Toplam Şablon"
|
||||
value={totalTemplates}
|
||||
color="purple"
|
||||
icon="FaClipboardList"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Aktif Şablon"
|
||||
value={activeTemplates}
|
||||
color="green"
|
||||
icon="FaClipboardList"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Pasif Şablon"
|
||||
value={totalTemplates - activeTemplates}
|
||||
color="red"
|
||||
icon="FaClipboardList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg border">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Şablon adı veya açıklama ile ara..."
|
||||
value={templateSearchTerm}
|
||||
onChange={(e) => setTemplateSearchTerm(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Templates Table */}
|
||||
<DataTable data={filteredTemplates} columns={templateColumns} />
|
||||
</div>
|
||||
|
||||
{/* Template Modal */}
|
||||
{showTemplateModal && (
|
||||
<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 justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold">
|
||||
{isEditMode ? 'Şablon Düzenle' : selectedTemplate ? 'Şablon Detayı' : 'Yeni Şablon'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowTemplateModal(false)}
|
||||
className="p-2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Basic Information */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Şablon Adı</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateForm.name}
|
||||
onChange={(e) => setTemplateForm({ ...templateForm, name: e.target.value })}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
disabled={!isEditMode && !!selectedTemplate}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Durum</label>
|
||||
<select
|
||||
value={templateForm.isActive ? 'active' : 'inactive'}
|
||||
onChange={(e) =>
|
||||
setTemplateForm({
|
||||
...templateForm,
|
||||
isActive: e.target.value === 'active',
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
disabled={!isEditMode && !!selectedTemplate}
|
||||
>
|
||||
<option value="active">Aktif</option>
|
||||
<option value="inactive">Pasif</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Açıklama</label>
|
||||
<textarea
|
||||
value={templateForm.description}
|
||||
onChange={(e) =>
|
||||
setTemplateForm({
|
||||
...templateForm,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
disabled={!isEditMode && !!selectedTemplate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Değerlendiriciler */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Değerlendirecekler
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{Object.values(AssessorTypeEnum).map((type) => {
|
||||
const labels = {
|
||||
[AssessorTypeEnum.Self]: 'Kendi',
|
||||
[AssessorTypeEnum.Manager]: 'Yönetici',
|
||||
[AssessorTypeEnum.Peer]: 'Meslektaş',
|
||||
[AssessorTypeEnum.Subordinate]: 'Ast',
|
||||
[AssessorTypeEnum.Customer]: 'Müşteri',
|
||||
[AssessorTypeEnum.OtherDepartment]: 'Diğer Departman',
|
||||
[AssessorTypeEnum.HRUpperManagement]: 'İK/Üst Yönetim',
|
||||
[AssessorTypeEnum.External]: 'Dış Paydaş',
|
||||
}
|
||||
|
||||
return (
|
||||
<label key={type} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={templateForm.assessorTypes.includes(type)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setTemplateForm({
|
||||
...templateForm,
|
||||
assessorTypes: [...templateForm.assessorTypes, type],
|
||||
})
|
||||
} else {
|
||||
setTemplateForm({
|
||||
...templateForm,
|
||||
assessorTypes: templateForm.assessorTypes.filter((t) => t !== type),
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
disabled={!isEditMode && !!selectedTemplate}
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{labels[type] || type}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question Groups */}
|
||||
{selectedTemplate && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">Soru Grupları</h3>
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={handleAddQuestionGroup}
|
||||
className="flex items-center gap-2 px-2 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-xs"
|
||||
>
|
||||
<FaPlus className="w-3 h-3" />
|
||||
Grup Ekle
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{selectedTemplate.questionGroups?.map((group, groupIndex) => (
|
||||
<div key={groupIndex} className="bg-gray-50 p-3 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-medium">{group.groupName}</h4>
|
||||
<p className="text-sm text-gray-600">{group.description}</p>
|
||||
<p className="text-gray-600">Ağırlık: {group.weight}%</p>
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleEditQuestionGroup(group)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAddQuestion(group)}
|
||||
className="text-green-600 hover:text-green-900"
|
||||
>
|
||||
<FaQuestionCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Questions */}
|
||||
<div className="space-y-2">
|
||||
{group.questions?.map((question, questionIndex) => (
|
||||
<div key={questionIndex} className="bg-white p-2 rounded border">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm">{question.questionText}</p>
|
||||
<div className="flex gap-4 mt-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
Tip:{' '}
|
||||
{question.questionType === QuestionTypeEnum.Rating
|
||||
? 'Puanlama'
|
||||
: question.questionType === QuestionTypeEnum.MultipleChoice
|
||||
? 'Çoktan Seçmeli'
|
||||
: question.questionType === QuestionTypeEnum.Text
|
||||
? 'Metin'
|
||||
: 'Evet/Hayır'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
Ağırlık: {question.weight}%
|
||||
</span>
|
||||
{question.isRequired && (
|
||||
<span className="text-xs text-red-500">Zorunlu</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={() => handleEditQuestion(question, group)}
|
||||
className="text-blue-600 hover:text-blue-900"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-4">
|
||||
<button
|
||||
onClick={() => setShowTemplateModal(false)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
{selectedTemplate && !isEditMode ? 'Kapat' : 'İptal'}
|
||||
</button>
|
||||
{(isEditMode || !selectedTemplate) && (
|
||||
<button
|
||||
onClick={handleSaveTemplate}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Kaydet
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Question Group Modal */}
|
||||
{showQuestionModal && (
|
||||
<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-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold">
|
||||
{selectedQuestionGroup ? 'Soru Grubu Düzenle' : 'Yeni Soru Grubu'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowQuestionModal(false)}
|
||||
className="p-2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Grup Adı</label>
|
||||
<input
|
||||
type="text"
|
||||
value={questionGroupForm.groupName}
|
||||
onChange={(e) =>
|
||||
setQuestionGroupForm({
|
||||
...questionGroupForm,
|
||||
groupName: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Açıklama</label>
|
||||
<textarea
|
||||
value={questionGroupForm.description}
|
||||
onChange={(e) =>
|
||||
setQuestionGroupForm({
|
||||
...questionGroupForm,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ağırlık (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={questionGroupForm.weight}
|
||||
onChange={(e) =>
|
||||
setQuestionGroupForm({
|
||||
...questionGroupForm,
|
||||
weight: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Sıra</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={questionGroupForm.order}
|
||||
onChange={(e) =>
|
||||
setQuestionGroupForm({
|
||||
...questionGroupForm,
|
||||
order: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-4">
|
||||
<button
|
||||
onClick={() => setShowQuestionModal(false)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveQuestionGroup}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Question Modal */}
|
||||
{showQuestionItemModal && (
|
||||
<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-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold">
|
||||
{selectedQuestion ? 'Soru Düzenle' : 'Yeni Soru'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowQuestionItemModal(false)}
|
||||
className="p-2 text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Soru Metni</label>
|
||||
<textarea
|
||||
value={questionForm.questionText}
|
||||
onChange={(e) =>
|
||||
setQuestionForm({
|
||||
...questionForm,
|
||||
questionText: e.target.value,
|
||||
})
|
||||
}
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Soru Tipi</label>
|
||||
<select
|
||||
value={questionForm.questionType}
|
||||
onChange={(e) =>
|
||||
setQuestionForm({
|
||||
...questionForm,
|
||||
questionType: e.target.value as QuestionTypeEnum,
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
{Object.values(QuestionTypeEnum).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getQuestionTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ağırlık (%)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={questionForm.weight}
|
||||
onChange={(e) =>
|
||||
setQuestionForm({
|
||||
...questionForm,
|
||||
weight: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Sıra</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={questionForm.order}
|
||||
onChange={(e) =>
|
||||
setQuestionForm({
|
||||
...questionForm,
|
||||
order: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isRequired"
|
||||
checked={questionForm.isRequired}
|
||||
onChange={(e) =>
|
||||
setQuestionForm({
|
||||
...questionForm,
|
||||
isRequired: e.target.checked,
|
||||
})
|
||||
}
|
||||
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isRequired" className="ml-2 block text-sm text-gray-700">
|
||||
Zorunlu soru
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-4">
|
||||
<button
|
||||
onClick={() => setShowQuestionItemModal(false)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveQuestion}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Degree360Templates
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { FaSave, FaTimes } from "react-icons/fa";
|
||||
import { DepartmentDto } from "../../../types/hr";
|
||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
||||
import { mockDepartments } from "../../../mocks/mockDepartments";
|
||||
import { mockCostCenters } from "../../../mocks/mockCostCenters";
|
||||
|
||||
interface DepartmentFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (department: Partial<DepartmentDto>) => void;
|
||||
department?: DepartmentDto;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const DepartmentFormModal: React.FC<DepartmentFormModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
department,
|
||||
title,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
departmentCode: "",
|
||||
name: "",
|
||||
description: "",
|
||||
parentDepartmentId: "",
|
||||
managerId: "",
|
||||
costCenterId: "",
|
||||
budget: 0,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFormData({
|
||||
departmentCode: department?.code || "",
|
||||
name: department?.name || "",
|
||||
description: department?.description || "",
|
||||
parentDepartmentId: department?.parentDepartmentId || "",
|
||||
managerId: department?.managerId || "",
|
||||
costCenterId: department?.costCenterId || "",
|
||||
budget: department?.budget || 0,
|
||||
isActive: department ? department.isActive : true,
|
||||
});
|
||||
setErrors({});
|
||||
}
|
||||
}, [department, isOpen]);
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.departmentCode.trim()) {
|
||||
newErrors.departmentCode = "Departman kodu zorunludur";
|
||||
} else if (
|
||||
mockDepartments.some(
|
||||
(d) => d.code === formData.departmentCode && d.id !== department?.id
|
||||
)
|
||||
) {
|
||||
newErrors.departmentCode = "Bu departman kodu zaten kullanılıyor";
|
||||
}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = "Departman adı zorunludur";
|
||||
}
|
||||
|
||||
if (formData.budget < 0) {
|
||||
newErrors.budget = "Bütçe negatif olamaz";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (validateForm()) {
|
||||
onSave(formData);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-3 w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-base font-bold text-gray-900">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
{/* Temel Bilgiler */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Departman Kodu *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.departmentCode}
|
||||
onChange={(e) =>
|
||||
handleInputChange("departmentCode", e.target.value)
|
||||
}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.departmentCode ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
placeholder="Örn: HR001"
|
||||
/>
|
||||
{errors.departmentCode && (
|
||||
<p className="text-red-500 text-xs mt-1">
|
||||
{errors.departmentCode}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Departman Adı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.name ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
placeholder="Örn: İnsan Kaynakları"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Açıklama
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
handleInputChange("description", e.target.value)
|
||||
}
|
||||
rows={2}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Departman açıklaması..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hiyerarşi ve İlişkiler */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Üst Departman
|
||||
</label>
|
||||
<select
|
||||
value={formData.parentDepartmentId}
|
||||
onChange={(e) =>
|
||||
handleInputChange("parentDepartmentId", e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Departman Seçin...</option>
|
||||
{mockDepartments
|
||||
.filter((d) => d.id !== department?.id)
|
||||
.map((dept) => (
|
||||
<option key={dept.id} value={dept.id}>
|
||||
{dept.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Departman Yöneticisi
|
||||
</label>
|
||||
<select
|
||||
value={formData.managerId}
|
||||
onChange={(e) =>
|
||||
handleInputChange("managerId", e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Yönetici Seçin</option>
|
||||
{mockEmployees.map((employee) => (
|
||||
<option key={employee.id} value={employee.id}>
|
||||
{employee.fullName} ({employee.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Maliyet Merkezi
|
||||
</label>
|
||||
<select
|
||||
value={formData.costCenterId}
|
||||
onChange={(e) =>
|
||||
handleInputChange("costCenterId", e.target.value)
|
||||
}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Maliyet Merkezi Seçin</option>
|
||||
{mockCostCenters.map((costCenter) => (
|
||||
<option key={costCenter.id} value={costCenter.id}>
|
||||
{costCenter.name} ({costCenter.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Bütçe (₺)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.budget}
|
||||
onChange={(e) =>
|
||||
handleInputChange("budget", parseFloat(e.target.value) || 0)
|
||||
}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.budget ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
{errors.budget && (
|
||||
<p className="text-red-500 text-xs mt-1">{errors.budget}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) =>
|
||||
handleInputChange("isActive", e.target.checked)
|
||||
}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 text-sm text-gray-700">
|
||||
Aktif departman
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex justify-end gap-2 pt-3 border-t">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentFormModal;
|
||||
|
|
@ -1,461 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import {
|
||||
FaPlus,
|
||||
FaEdit,
|
||||
FaTrash,
|
||||
FaBuilding,
|
||||
FaUsers,
|
||||
FaDollarSign,
|
||||
FaEye,
|
||||
FaList,
|
||||
FaTh,
|
||||
} from 'react-icons/fa'
|
||||
import { DepartmentDto } from '../../../types/hr'
|
||||
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||
import { mockDepartments } from '../../../mocks/mockDepartments'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { mockCostCenters } from '../../../mocks/mockCostCenters'
|
||||
import DepartmentFormModal from './DepartmentFormModal'
|
||||
import DepartmentViewModal from './DepartmentViewModal'
|
||||
import Widget from '../../../components/common/Widget'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const DepartmentManagement: React.FC = () => {
|
||||
const [departments, setDepartments] = useState<DepartmentDto[]>(mockDepartments)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedParent, setSelectedParent] = useState<string>('all')
|
||||
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
|
||||
|
||||
// Modal states
|
||||
const [isFormModalOpen, setIsFormModalOpen] = useState(false)
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
||||
const [selectedDepartment, setSelectedDepartment] = useState<DepartmentDto | undefined>()
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
|
||||
const handleAdd = () => {
|
||||
setSelectedDepartment(undefined)
|
||||
setModalTitle('Yeni Departman')
|
||||
setIsFormModalOpen(true)
|
||||
}
|
||||
|
||||
const handleEdit = (department: DepartmentDto) => {
|
||||
setSelectedDepartment(department)
|
||||
setModalTitle('Departman Düzenle')
|
||||
setIsFormModalOpen(true)
|
||||
}
|
||||
|
||||
const handleView = (department: DepartmentDto) => {
|
||||
setSelectedDepartment(department)
|
||||
setIsViewModalOpen(true)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (confirm('Bu departmanı silmek istediğinizden emin misiniz?')) {
|
||||
setDepartments((prev) => prev.filter((dept) => dept.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = (departmentData: Partial<DepartmentDto>) => {
|
||||
if (selectedDepartment) {
|
||||
// Edit existing department
|
||||
setDepartments((prev) =>
|
||||
prev.map((dept) =>
|
||||
dept.id === selectedDepartment.id
|
||||
? { ...dept, ...departmentData, lastModificationTime: new Date() }
|
||||
: dept,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// Add new department
|
||||
const newDepartment: DepartmentDto = {
|
||||
id: `dept_${Date.now()}`,
|
||||
...departmentData,
|
||||
subDepartments: [],
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
} as DepartmentDto
|
||||
setDepartments((prev) => [...prev, newDepartment])
|
||||
}
|
||||
setIsFormModalOpen(false)
|
||||
}
|
||||
|
||||
const handleCloseFormModal = () => {
|
||||
setIsFormModalOpen(false)
|
||||
setSelectedDepartment(undefined)
|
||||
}
|
||||
|
||||
const handleCloseViewModal = () => {
|
||||
setIsViewModalOpen(false)
|
||||
setSelectedDepartment(undefined)
|
||||
}
|
||||
|
||||
const handleEditFromView = (department: DepartmentDto) => {
|
||||
setIsViewModalOpen(false)
|
||||
handleEdit(department)
|
||||
}
|
||||
|
||||
mockDepartments.forEach((dept) => {
|
||||
if (dept.managerId) {
|
||||
dept.manager = mockEmployees.find((emp) => emp.id === dept.managerId)
|
||||
}
|
||||
if (dept.costCenterId) {
|
||||
dept.costCenter = mockCostCenters.find((cc) => cc.id === dept.costCenterId)
|
||||
}
|
||||
})
|
||||
|
||||
const filteredDepartments = departments.filter((department) => {
|
||||
if (
|
||||
searchTerm &&
|
||||
!department.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||
!department.code.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (selectedParent !== 'all' && department.parentDepartmentId !== selectedParent) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const columns: Column<DepartmentDto>[] = [
|
||||
{
|
||||
key: 'code',
|
||||
header: 'Departman Kodu',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
header: 'Departman Adı',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'parentDepartment',
|
||||
header: 'Üst Departman',
|
||||
render: (department: DepartmentDto) => department.parentDepartment?.name || '-',
|
||||
},
|
||||
{
|
||||
key: 'manager',
|
||||
header: 'Yönetici',
|
||||
render: (department: DepartmentDto) => department.manager?.fullName || '-',
|
||||
},
|
||||
{
|
||||
key: 'employeeCount',
|
||||
header: 'Personel Sayısı',
|
||||
render: (department: DepartmentDto) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaUsers className="w-4 h-4 text-gray-500" />
|
||||
<span>{mockEmployees.filter((a) => a.departmentId == department.id).length || 0}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'costCenter',
|
||||
header: 'Maliyet Merkezi',
|
||||
render: (department: DepartmentDto) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{department.costCenter?.code || '-'}</span>
|
||||
<span className="text-xs text-gray-500">{department.costCenter?.name || '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'budget',
|
||||
header: 'Bütçe',
|
||||
render: (department: DepartmentDto) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
||||
<span>{department.budget ? `₺${department.budget.toLocaleString()}` : '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Durum',
|
||||
render: (department: DepartmentDto) => (
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
department.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{department.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'İşlemler',
|
||||
render: (department: DepartmentDto) => (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleView(department)}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(department)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(department.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Departman Yönetimi</h2>
|
||||
<p className="text-gray-600">
|
||||
Organizasyon yapısını ve departmanları yönetin
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex bg-gray-100 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
viewMode === 'list'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title="Liste Görünümü"
|
||||
>
|
||||
<FaList className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('cards')}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
viewMode === 'cards'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title="Kart Görünümü"
|
||||
>
|
||||
<FaTh className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<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 Departman
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<Widget
|
||||
title="Toplam Departman"
|
||||
value={departments.length}
|
||||
color="blue"
|
||||
icon="FaBuilding"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Toplam Personel"
|
||||
value={mockEmployees.length}
|
||||
color="green"
|
||||
icon="FaUsers"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Ana Departmanlar"
|
||||
value={departments.filter((d) => !d.parentDepartmentId).length}
|
||||
color="purple"
|
||||
icon="FaBuilding"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Toplam Bütçe"
|
||||
value={`₺${departments
|
||||
.reduce((total, dept) => total + (dept.budget || 0), 0)
|
||||
.toLocaleString()}`}
|
||||
color="orange"
|
||||
icon="FaDollarSign"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Departman adı veya kodu ara..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(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>
|
||||
|
||||
<select
|
||||
value={selectedParent}
|
||||
onChange={(e) => setSelectedParent(e.target.value)}
|
||||
className="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[200px]"
|
||||
>
|
||||
<option value="all">Tüm Departmanlar</option>
|
||||
{departments
|
||||
.filter((d) => !d.parentDepartmentId)
|
||||
.map((dept) => (
|
||||
<option key={dept.id} value={dept.id}>
|
||||
{dept.name} Alt Departmanları
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Data Display */}
|
||||
{viewMode === 'list' ? (
|
||||
<div className="bg-white rounded-lg shadow-sm border overflow-x-auto">
|
||||
<DataTable data={filteredDepartments} columns={columns} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{filteredDepartments.map((department) => (
|
||||
<div
|
||||
key={department.id}
|
||||
className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow p-3"
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-lg flex items-center justify-center ${
|
||||
department.isActive
|
||||
? 'bg-blue-100 text-blue-600'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
<FaBuilding className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm text-gray-900 truncate">
|
||||
{department.name}
|
||||
</h3>
|
||||
<p className="text-gray-600">{department.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
department.isActive
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{department.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Card Content */}
|
||||
<div className="space-y-2">
|
||||
{department.parentDepartment && (
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaBuilding className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>Üst: {department.parentDepartment.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{department.manager && (
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaUsers className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>Yönetici: {department.manager.fullName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaUsers className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>
|
||||
{mockEmployees.filter((emp) => emp.departmentId === department.id).length}{' '}
|
||||
Personel
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{department.budget && (
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaDollarSign className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>₺{department.budget.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{department.costCenter && (
|
||||
<div className="text-xs text-gray-600">
|
||||
<div className="font-medium">{department.costCenter.code}</div>
|
||||
<div className="text-xs text-gray-500">{department.costCenter.name}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Card Actions */}
|
||||
<div className="flex justify-end space-x-1 mt-3 pt-3 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => handleView(department)}
|
||||
className="p-1.5 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(department)}
|
||||
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(department.id)}
|
||||
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredDepartments.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<FaBuilding className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Departman bulunamadı</h3>
|
||||
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<DepartmentFormModal
|
||||
isOpen={isFormModalOpen}
|
||||
onClose={handleCloseFormModal}
|
||||
onSave={handleSave}
|
||||
department={selectedDepartment}
|
||||
title={modalTitle}
|
||||
/>
|
||||
|
||||
<DepartmentViewModal
|
||||
isOpen={isViewModalOpen}
|
||||
onClose={handleCloseViewModal}
|
||||
department={selectedDepartment || null}
|
||||
onEdit={handleEditFromView}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default DepartmentManagement
|
||||
|
|
@ -1,272 +0,0 @@
|
|||
import React from "react";
|
||||
import {
|
||||
FaTimes,
|
||||
FaBuilding,
|
||||
FaUser,
|
||||
FaDollarSign,
|
||||
FaUsers,
|
||||
FaCalendar,
|
||||
FaEdit,
|
||||
} from "react-icons/fa";
|
||||
import { DepartmentDto } from "../../../types/hr";
|
||||
import { mockEmployees } from "../../../mocks/mockEmployees";
|
||||
|
||||
interface DepartmentViewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
department: DepartmentDto | null;
|
||||
onEdit?: (department: DepartmentDto) => void;
|
||||
}
|
||||
|
||||
const DepartmentViewModal: React.FC<DepartmentViewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
department,
|
||||
onEdit,
|
||||
}) => {
|
||||
const employeeCount = mockEmployees.filter(
|
||||
(emp) => emp.departmentId === department?.id
|
||||
).length;
|
||||
const departmentEmployees = mockEmployees.filter(
|
||||
(emp) => emp.departmentId === department?.id
|
||||
);
|
||||
|
||||
return (
|
||||
isOpen &&
|
||||
department && (
|
||||
<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">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-base font-bold text-gray-900">
|
||||
Departman Detayları
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={() => onEdit(department)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 bg-blue-50 rounded-md hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
Düzenle
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Temel Bilgiler */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<FaBuilding className="w-5 h-5" />
|
||||
Temel Bilgiler
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Departman Kodu
|
||||
</label>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{department.code}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Departman Adı
|
||||
</label>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{department.name}
|
||||
</p>
|
||||
</div>
|
||||
{department.description && (
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Açıklama
|
||||
</label>
|
||||
<p className="text-sm text-gray-900 mt-1">
|
||||
{department.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Durum
|
||||
</label>
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${
|
||||
department.isActive
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{department.isActive ? "Aktif" : "Pasif"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hiyerarşi ve İlişkiler */}
|
||||
<div className="bg-blue-50 rounded-lg p-3">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<FaUser className="w-5 h-5" />
|
||||
Hiyerarşi ve İlişkiler
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Üst Departman
|
||||
</label>
|
||||
<p className="text-gray-900 mt-1">
|
||||
{department.parentDepartment?.name || "Ana Departman"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Departman Yöneticisi
|
||||
</label>
|
||||
<p className="text-gray-900 mt-1">
|
||||
{department.manager?.fullName || "Atanmamış"}
|
||||
</p>
|
||||
{department.manager && (
|
||||
<p className="text-sm text-gray-500">
|
||||
{department.manager.code}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Maliyet Merkezi
|
||||
</label>
|
||||
<p className="text-gray-900 mt-1">
|
||||
{department.costCenter?.name || "Atanmamış"}
|
||||
</p>
|
||||
{department.costCenter && (
|
||||
<p className="text-sm text-gray-500">
|
||||
{department.costCenter.code}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Finansal Bilgiler */}
|
||||
<div className="bg-green-50 rounded-lg p-3">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<FaDollarSign className="w-5 h-5" />
|
||||
Finansal Bilgiler
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Departman Bütçesi
|
||||
</label>
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{department.budget
|
||||
? `₺${department.budget.toLocaleString()}`
|
||||
: "Belirtilmemiş"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Personel Bilgileri */}
|
||||
<div className="bg-purple-50 rounded-lg p-3">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<FaUsers className="w-5 h-5" />
|
||||
Personel Bilgileri
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Toplam Personel Sayısı
|
||||
</label>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
{employeeCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{departmentEmployees.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<label className="block text-sm font-medium text-gray-600 mb-2">
|
||||
Departman Personeli
|
||||
</label>
|
||||
<div className="max-h-32 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{departmentEmployees.map((employee) => (
|
||||
<div
|
||||
key={employee.id}
|
||||
className="flex items-center justify-between p-1.5 bg-white rounded border"
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">
|
||||
{employee.fullName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{employee.code}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{employee.jobPosition?.name ||
|
||||
"Pozisyon belirtilmemiş"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sistem Bilgileri */}
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-3 flex items-center gap-2">
|
||||
<FaCalendar className="w-5 h-5" />
|
||||
Sistem Bilgileri
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Oluşturulma Tarihi
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{new Date(department.creationTime).toLocaleDateString(
|
||||
"tr-TR"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600">
|
||||
Son Güncelleme
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{new Date(
|
||||
department.lastModificationTime
|
||||
).toLocaleDateString("tr-TR")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
<div className="flex justify-end pt-3 border-t mt-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentViewModal;
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import {
|
||||
FaUser,
|
||||
FaEnvelope,
|
||||
FaPhone,
|
||||
FaCalendar,
|
||||
FaCertificate,
|
||||
FaEdit,
|
||||
FaEye,
|
||||
FaTrash,
|
||||
FaPlus,
|
||||
} from 'react-icons/fa'
|
||||
import { EmployeeDto, EmployeeStatusEnum } from '../../../types/hr'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { getEmployeeStatusColor, getEmployeeStatusText } from '../../../utils/erp'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const EmployeeCards: React.FC = () => {
|
||||
const [employees] = useState<EmployeeDto[]>(mockEmployees)
|
||||
const [selectedDepartment, setSelectedDepartment] = useState<string>('all')
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all')
|
||||
|
||||
const handleEdit = (employee: EmployeeDto) => {
|
||||
console.log('Edit employee:', employee)
|
||||
// Implement edit functionality
|
||||
}
|
||||
|
||||
const handleView = (employee: EmployeeDto) => {
|
||||
console.log('View employee:', employee)
|
||||
// Implement view functionality
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
console.log('Delete employee:', id)
|
||||
// Implement delete functionality
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
console.log('Add new employee')
|
||||
// Implement add functionality
|
||||
}
|
||||
|
||||
const filteredEmployees = employees.filter((employee) => {
|
||||
if (selectedDepartment !== 'all' && employee.department?.id !== selectedDepartment) {
|
||||
return false
|
||||
}
|
||||
if (selectedStatus !== 'all' && employee.employeeStatus !== selectedStatus) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header with Add Button */}
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-gray-900">Personel Kartları</h2>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className="px-2 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
Yeni Personel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-3 mb-4">
|
||||
<select
|
||||
value={selectedDepartment}
|
||||
onChange={(e) => setSelectedDepartment(e.target.value)}
|
||||
className="px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Tüm Departmanlar</option>
|
||||
{/* Department options would be populated from departments list */}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className="px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Tüm Durumlar</option>
|
||||
{Object.values(EmployeeStatusEnum).map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getEmployeeStatusText(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Employee Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredEmployees.map((employee) => (
|
||||
<div
|
||||
key={employee.id}
|
||||
className="bg-white rounded-lg shadow-sm p-3 hover:shadow-md transition-shadow border"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<FaUser className="w-6 h-6 text-blue-600" />
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getEmployeeStatusColor(
|
||||
employee.employeeStatus,
|
||||
)}`}
|
||||
>
|
||||
{getEmployeeStatusText(employee.employeeStatus)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Employee Info */}
|
||||
<div className="space-y-1 mb-3">
|
||||
<h3 className="font-semibold text-sm text-gray-900">{employee.fullName}</h3>
|
||||
<p className="text-xs text-gray-600">{employee.jobPosition?.name}</p>
|
||||
<p className="text-sm text-gray-500">{employee.department?.name}</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="space-y-1.5 mb-3">
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaEnvelope className="w-4 h-4 mr-2" />
|
||||
<span className="truncate">{employee.email}</span>
|
||||
</div>
|
||||
{employee.phone && (
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaPhone className="w-4 h-4 mr-2" />
|
||||
<span>{employee.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaCalendar className="w-4 h-4 mr-2" />
|
||||
<span>{new Date(employee.hireDate).toLocaleDateString('tr-TR')}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-gray-600">
|
||||
<FaCertificate className="w-4 h-4 mr-2" />
|
||||
<span>{employee.code}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-3 border-t">
|
||||
<button
|
||||
onClick={() => handleView(employee)}
|
||||
className="flex-1 px-2 py-1 text-xs bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
Görüntüle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(employee)}
|
||||
className="flex-1 px-2 py-1 text-xs bg-green-50 text-green-600 rounded-md hover:bg-green-100 transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
Düzenle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(employee.id)}
|
||||
className="px-2 py-1 text-xs bg-red-50 text-red-600 rounded-md hover:bg-red-100 transition-colors flex items-center justify-center"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredEmployees.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<FaUser className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">Personel bulunamadı</h3>
|
||||
<p className="text-gray-500">Seçilen kriterlere uygun personel bulunmamaktadır.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmployeeCards
|
||||
|
|
@ -1,830 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
FaSave,
|
||||
FaTimes,
|
||||
FaUser,
|
||||
FaBriefcase,
|
||||
FaDollarSign,
|
||||
FaMapMarkerAlt,
|
||||
FaPhone,
|
||||
FaEnvelope,
|
||||
} from 'react-icons/fa'
|
||||
import LoadingSpinner from '../../../components/common/LoadingSpinner'
|
||||
import { mockDepartments } from '../../../mocks/mockDepartments'
|
||||
import { mockJobPositions } from '../../../mocks/mockJobPositions'
|
||||
import {
|
||||
DepartmentDto,
|
||||
EmployeeDto,
|
||||
EmployeeStatusEnum,
|
||||
EmploymentTypeEnum,
|
||||
GenderEnum,
|
||||
JobLevelEnum,
|
||||
JobPositionDto,
|
||||
MaritalStatusEnum,
|
||||
} from '../../../types/hr'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { Container } from '@/components/shared'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { mockCurrencies } from '@/mocks/mockCurrencies'
|
||||
import {
|
||||
getEmployeeStatusText,
|
||||
getEmploymentTypeText,
|
||||
getFrequencyUnitText,
|
||||
getGenderText,
|
||||
getMaritalStatusText,
|
||||
} from '@/utils/erp'
|
||||
import { FrequencyUnitEnum } from '@/types/pm'
|
||||
|
||||
interface ValidationErrors {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const EmployeeForm: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const isEdit = Boolean(id)
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [errors, setErrors] = useState<ValidationErrors>({})
|
||||
const [departments, setDepartments] = useState<DepartmentDto[]>([])
|
||||
const [jobPositions, setJobPositions] = useState<JobPositionDto[]>([])
|
||||
const [managers, setManagers] = useState<EmployeeDto[]>([])
|
||||
|
||||
const [formData, setFormData] = useState<EmployeeDto>({
|
||||
id: '',
|
||||
code: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
personalPhone: '',
|
||||
nationalId: '',
|
||||
birthDate: new Date(),
|
||||
gender: GenderEnum.Male,
|
||||
maritalStatus: MaritalStatusEnum.Single,
|
||||
address: {
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postalCode: '',
|
||||
country: '',
|
||||
},
|
||||
emergencyContact: {
|
||||
name: '',
|
||||
relationship: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: {
|
||||
street: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postalCode: '',
|
||||
country: '',
|
||||
},
|
||||
},
|
||||
hireDate: new Date(),
|
||||
employmentType: EmploymentTypeEnum.FullTime,
|
||||
jobPositionId: '',
|
||||
departmentId: '',
|
||||
baseSalary: 0,
|
||||
currency: 'TRY',
|
||||
payrollGroup: '',
|
||||
bankAccountId: '',
|
||||
workLocation: '',
|
||||
badgeNumber: '',
|
||||
employeeStatus: EmployeeStatusEnum.Active,
|
||||
isActive: true,
|
||||
leaves: [],
|
||||
evaluations: [],
|
||||
trainings: [],
|
||||
disciplinaryActions: [],
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
})
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const mockManagers = mockEmployees.filter(
|
||||
(emp) => emp.jobPosition?.level === JobLevelEnum.Manager,
|
||||
)
|
||||
|
||||
setDepartments(mockDepartments)
|
||||
setJobPositions(mockJobPositions)
|
||||
setManagers(mockManagers)
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loadFormData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (isEdit && id) {
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// Mock employee data
|
||||
const mockEmployee = mockEmployees.find((emp) => emp.id === id)!
|
||||
setFormData(mockEmployee)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading form data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [isEdit, id])
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
loadFormData()
|
||||
}, [loadData, loadFormData])
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: ValidationErrors = {}
|
||||
|
||||
if (!formData.code.trim()) {
|
||||
newErrors.code = 'Personel kodu zorunludur'
|
||||
}
|
||||
if (!formData.firstName.trim()) {
|
||||
newErrors.firstName = 'Ad zorunludur'
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
newErrors.lastName = 'Soyad zorunludur'
|
||||
}
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email zorunludur'
|
||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
newErrors.email = 'Geçerli bir email adresi girin'
|
||||
}
|
||||
if (!formData.nationalId.trim()) {
|
||||
newErrors.nationalId = 'TC Kimlik No zorunludur'
|
||||
} else if (formData.nationalId.length !== 11) {
|
||||
newErrors.nationalId = 'TC Kimlik No 11 haneli olmalıdır'
|
||||
}
|
||||
if (!formData.birthDate) {
|
||||
newErrors.birthDate = 'Doğum tarihi zorunludur'
|
||||
}
|
||||
if (!formData.hireDate) {
|
||||
newErrors.hireDate = 'İşe giriş tarihi zorunludur'
|
||||
}
|
||||
if (!formData.departmentId) {
|
||||
newErrors.departmentId = 'Departman seçilmelidir'
|
||||
}
|
||||
if (!formData.jobPosition) {
|
||||
newErrors.jobPositionId = 'Pozisyon seçilmelidir'
|
||||
}
|
||||
if (!formData.employmentType) {
|
||||
newErrors.employmentType = 'Çalışma tipi seçilmelidir'
|
||||
}
|
||||
if (formData.baseSalary <= 0) {
|
||||
newErrors.baseSalary = "Maaş 0'dan büyük olmalıdır"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleInputChange = (field: keyof EmployeeDto, value: string | number | boolean) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}))
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[field]: '',
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
|
||||
console.log('Employee data:', {
|
||||
...formData,
|
||||
id: isEdit ? id : undefined,
|
||||
})
|
||||
|
||||
// Show success message
|
||||
alert(isEdit ? 'Personel başarıyla güncellendi!' : 'Personel başarıyla oluşturuldu!')
|
||||
|
||||
// Navigate back to list
|
||||
navigate(ROUTES_ENUM.protected.hr.employees)
|
||||
} catch (error) {
|
||||
console.error('Error saving employee:', error)
|
||||
alert('Bir hata oluştu. Lütfen tekrar deneyin.')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate(ROUTES_ENUM.protected.hr.employees)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{isEdit ? 'Personel Düzenle' : 'Yeni Personel'}
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
{isEdit
|
||||
? 'Mevcut personel bilgilerini güncelleyin'
|
||||
: 'Yeni personel bilgilerini girin'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
{/* Personal Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-3 py-2 border-b border-gray-200">
|
||||
<h3 className="text-base font-medium text-gray-900 flex items-center">
|
||||
<FaUser className="w-5 h-5 mr-2" />
|
||||
Kişisel Bilgiler
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Personel Kodu *
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={formData.code}
|
||||
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.code
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Örn: EMP001"
|
||||
/>
|
||||
{errors.employeeCode && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.employeeCode}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
TC Kimlik No *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={11}
|
||||
value={formData.nationalId}
|
||||
onChange={(e) => handleInputChange('nationalId', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.nationalId
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="12345678901"
|
||||
/>
|
||||
{errors.nationalId && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.nationalId}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Ad *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.firstName
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Ad"
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.firstName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Soyad *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.lastName
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="Soyad"
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.lastName}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Doğum Tarihi *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.birthDate && formData.birthDate.toString().split('T')[0]}
|
||||
onChange={(e) => handleInputChange('birthDate', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.birthDate
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
/>
|
||||
{errors.birthDate && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.birthDate}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Cinsiyet</label>
|
||||
<select
|
||||
value={formData.gender}
|
||||
onChange={(e) => handleInputChange('gender', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Cinsiyet seçin</option>
|
||||
{Object.values(GenderEnum).map((gender) => (
|
||||
<option key={gender} value={gender}>
|
||||
{getGenderText(gender)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Medeni Durum
|
||||
</label>
|
||||
<select
|
||||
value={formData.maritalStatus}
|
||||
onChange={(e) => handleInputChange('maritalStatus', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Medeni durum seçin</option>
|
||||
{Object.values(MaritalStatusEnum).map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getMaritalStatusText(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-3 py-2 border-b border-gray-200">
|
||||
<h3 className="text-base font-medium text-gray-900 flex items-center">
|
||||
<FaPhone className="w-5 h-5 mr-2" />
|
||||
İletişim Bilgileri
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email *</label>
|
||||
<div className="relative">
|
||||
<FaEnvelope className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className={`block w-full pl-10 pr-3 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.email
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="email@company.com"
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="mt-1 text-sm text-red-600">{errors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
İş Telefonu
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="+90 212 555 0123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kişisel Telefon
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.personalPhone}
|
||||
onChange={(e) => handleInputChange('personalPhone', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="+90 532 555 0123"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-3 py-2 border-b border-gray-200">
|
||||
<h3 className="text-base font-medium text-gray-900 flex items-center">
|
||||
<FaMapMarkerAlt className="w-5 h-5 mr-2" />
|
||||
Adres Bilgileri
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Employment Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-3 py-2 border-b border-gray-200">
|
||||
<h3 className="text-base font-medium text-gray-900 flex items-center">
|
||||
<FaBriefcase className="w-5 h-5 mr-2" />
|
||||
İş Bilgileri
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
İşe Giriş Tarihi *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.hireDate && formData.hireDate.toString().split('T')[0]}
|
||||
onChange={(e) => handleInputChange('hireDate', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.hireDate
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
/>
|
||||
{errors.hireDate && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.hireDate}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Çalışma Tipi *
|
||||
</label>
|
||||
<select
|
||||
value={formData.employmentType}
|
||||
onChange={(e) => handleInputChange('employmentType', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.employmentType
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
<option value="">Tip seçin</option>
|
||||
{Object.values(EmploymentTypeEnum).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getEmploymentTypeText(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.employmentType && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.employmentType}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Departman *
|
||||
</label>
|
||||
<select
|
||||
value={formData.departmentId}
|
||||
onChange={(e) => handleInputChange('departmentId', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.departmentId
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
<option value="">Departman seçin</option>
|
||||
{departments.map((dept) => (
|
||||
<option key={dept.id} value={dept.id}>
|
||||
{dept.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.departmentId && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.departmentId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Pozisyon *</label>
|
||||
<select
|
||||
value={formData.jobPositionId}
|
||||
onChange={(e) => handleInputChange('jobPositionId', e.target.value)}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.jobPositionId
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
>
|
||||
<option value="">Pozisyon seçin</option>
|
||||
{jobPositions.map((position) => (
|
||||
<option key={position.id} value={position.id}>
|
||||
{position.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.jobPositionId && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.jobPositionId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Yönetici</label>
|
||||
<select
|
||||
value={formData.managerId}
|
||||
onChange={(e) => handleInputChange('managerId', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Yönetici seçin</option>
|
||||
{managers.map((manager) => (
|
||||
<option key={manager.id} value={manager.id}>
|
||||
{manager.fullName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Çalışma Lokasyonu
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.workLocation}
|
||||
onChange={(e) => handleInputChange('workLocation', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="Ofis lokasyonu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kart Numarası
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.badgeNumber}
|
||||
onChange={(e) => handleInputChange('badgeNumber', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="Personel kartı numarası"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Salary Information */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-3 py-2 border-b border-gray-200">
|
||||
<h3 className="text-base font-medium text-gray-900 flex items-center">
|
||||
<FaDollarSign className="w-5 h-5 mr-2" />
|
||||
Maaş Bilgileri
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Temel Maaş *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={formData.baseSalary}
|
||||
onChange={(e) =>
|
||||
handleInputChange('baseSalary', parseFloat(e.target.value) || 0)
|
||||
}
|
||||
className={`block w-full px-2 py-1 text-sm border rounded-md shadow-sm focus:outline-none focus:ring-1 ${
|
||||
errors.baseSalary
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
||||
}`}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
{errors.baseSalary && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.baseSalary}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Para Birimi
|
||||
</label>
|
||||
<select
|
||||
value={formData.currency}
|
||||
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
{mockCurrencies.map((currency) => (
|
||||
<option key={currency.value} value={currency.value}>
|
||||
{currency.value} - {currency.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bordro Grubu
|
||||
</label>
|
||||
<select
|
||||
value={formData.payrollGroup}
|
||||
onChange={(e) => handleInputChange('payrollGroup', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Grup seçin</option>
|
||||
<option value={FrequencyUnitEnum.Weeks}>
|
||||
{getFrequencyUnitText(FrequencyUnitEnum.Weeks)}
|
||||
</option>
|
||||
<option value={FrequencyUnitEnum.Months}>
|
||||
{getFrequencyUnitText(FrequencyUnitEnum.Months)}
|
||||
</option>
|
||||
<option value={FrequencyUnitEnum.Hours}>
|
||||
{getFrequencyUnitText(FrequencyUnitEnum.Hours)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Emergency Contact & Status */}
|
||||
<div className="bg-white shadow rounded-lg">
|
||||
<div className="px-3 py-2 border-b border-gray-200">
|
||||
<h3 className="text-base font-medium text-gray-900 flex items-center">
|
||||
<FaPhone className="w-5 h-5 mr-2" />
|
||||
Acil Durum İletişim & Durum
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Acil Durum Kişisi
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.emergencyContact.name}
|
||||
onChange={(e) => handleInputChange('emergencyContact.name', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="Ad Soyad"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Acil Durum Telefonu
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.emergencyContact.phone}
|
||||
onChange={(e) => handleInputChange('emergencyContact.phone', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="+90 532 555 0123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Yakınlık Derecesi
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.emergencyContact.relationship}
|
||||
onChange={(e) =>
|
||||
handleInputChange('emergencyContact.relationship', e.target.value)
|
||||
}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
placeholder="Eş, Anne, Baba vs."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Personel Durumu
|
||||
</label>
|
||||
<select
|
||||
value={formData.employeeStatus}
|
||||
onChange={(e) => handleInputChange('employeeStatus', e.target.value)}
|
||||
className="block w-full px-2 py-1 text-sm border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:border-blue-500 focus:ring-blue-500"
|
||||
>
|
||||
{Object.values(EmployeeStatusEnum).map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getEmployeeStatusText(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center pt-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
|
||||
Aktif
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex items-center justify-end space-x-2 bg-gray-50 px-3 py-2 rounded-b-lg border-t">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<FaTimes className="w-4 h-4 mr-2" />
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Kaydediliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaSave className="w-4 h-4 mr-2" />
|
||||
{isEdit ? 'Güncelle' : 'Kaydet'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmployeeForm
|
||||
|
|
@ -1,564 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
FaUsers,
|
||||
FaPlus,
|
||||
FaSearch,
|
||||
FaFilter,
|
||||
FaDownload,
|
||||
FaEdit,
|
||||
FaEye,
|
||||
FaPhone,
|
||||
FaEnvelope,
|
||||
FaCalendar,
|
||||
FaBuilding,
|
||||
FaAward,
|
||||
FaExclamationTriangle,
|
||||
FaList,
|
||||
FaTh,
|
||||
FaBriefcase,
|
||||
} from 'react-icons/fa'
|
||||
import classNames from 'classnames'
|
||||
import { EmployeeStatusEnum, EmployeeDto } from '../../../types/hr'
|
||||
import dayjs from 'dayjs'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import EmployeeView from './EmployeeView'
|
||||
import Widget from '../../../components/common/Widget'
|
||||
import {
|
||||
getEmploymentTypeColor,
|
||||
getEmploymentTypeText,
|
||||
getEmployeeStatusColor,
|
||||
getEmployeeStatusIcon,
|
||||
getEmployeeStatusText,
|
||||
} from '../../../utils/erp'
|
||||
import { Container } from '@/components/shared'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { mockDepartments } from '@/mocks/mockDepartments'
|
||||
|
||||
const EmployeeList: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState('all')
|
||||
const [filterDepartment, setFilterDepartment] = useState('all')
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
const [viewMode, setViewMode] = useState<'list' | 'cards'>('list')
|
||||
|
||||
// Modal states
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
||||
const [selectedEmployee, setSelectedEmployee] = useState<EmployeeDto | null>(null)
|
||||
|
||||
const {
|
||||
data: employees,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['employees', searchTerm, filterStatus, filterDepartment],
|
||||
queryFn: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
return mockEmployees.filter((employee) => {
|
||||
const matchesSearch =
|
||||
employee.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
employee.fullName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
employee.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
const matchesStatus = filterStatus === 'all' || employee.employeeStatus === filterStatus
|
||||
const matchesDepartment =
|
||||
filterDepartment === 'all' || employee.department?.code === filterDepartment
|
||||
return matchesSearch && matchesStatus && matchesDepartment
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Modal handlers
|
||||
const handleViewEmployee = (employee: EmployeeDto) => {
|
||||
setSelectedEmployee(employee)
|
||||
setIsViewModalOpen(true)
|
||||
}
|
||||
|
||||
const handleCloseViewModal = () => {
|
||||
setIsViewModalOpen(false)
|
||||
setSelectedEmployee(null)
|
||||
}
|
||||
|
||||
const handleEditFromView = (employee: EmployeeDto) => {
|
||||
setIsViewModalOpen(false)
|
||||
// Navigate to edit page - you can replace this with a modal if preferred
|
||||
window.location.href = ROUTES_ENUM.protected.hr.employeesEdit.replace(':id', employee.id)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-3 text-gray-600">Personel listesi yükleniyor...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-center">
|
||||
<FaExclamationTriangle className="h-5 w-5 text-red-600 mr-2" />
|
||||
<span className="text-red-800">Personel listesi yüklenirken hata oluştu.</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
{/* Title & Description */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">Personel Listesi</h2>
|
||||
<p className="text-gray-600">Şirket çalışanlarının listesi</p>
|
||||
</div>
|
||||
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex bg-gray-100 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
viewMode === 'list'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title="Liste Görünümü"
|
||||
>
|
||||
<FaList className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('cards')}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
viewMode === 'cards'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
title="Kart Görünümü"
|
||||
>
|
||||
<FaTh className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className="relative">
|
||||
<FaSearch
|
||||
size={18}
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Personel kodu, ad veya e-posta..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 pr-4 py-1 text-sm w-full sm:w-64 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filter Button */}
|
||||
<button
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className={classNames(
|
||||
'flex items-center px-3 py-1.5 text-sm border rounded-lg transition-colors',
|
||||
showFilters
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700'
|
||||
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
|
||||
)}
|
||||
>
|
||||
<FaFilter size={16} className="mr-2" />
|
||||
Filtreler
|
||||
</button>
|
||||
|
||||
{/* Export Button */}
|
||||
<button
|
||||
onClick={() => alert('Dışa aktarma özelliği yakında eklenecek')}
|
||||
className="flex items-center px-3 py-1 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<FaDownload size={16} className="mr-2" />
|
||||
Dışa Aktar
|
||||
</button>
|
||||
|
||||
{/* Add New Employee */}
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.hr.employeesNew}
|
||||
className="flex items-center px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlus size={16} className="mr-2" />
|
||||
Yeni Personel
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters Panel */}
|
||||
{showFilters && (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Durum</label>
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Tümü</option>
|
||||
{Object.values(EmployeeStatusEnum).map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getEmployeeStatusText(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Departman</label>
|
||||
<select
|
||||
value={filterDepartment}
|
||||
onChange={(e) => setFilterDepartment(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="all">Tümü</option>
|
||||
{mockDepartments.map((dept) => (
|
||||
<option key={dept.id} value={dept.code}>
|
||||
{dept.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
setFilterStatus('all')
|
||||
setFilterDepartment('all')
|
||||
setSearchTerm('')
|
||||
}}
|
||||
className="w-full px-3 py-1 text-sm border border-gray-300 bg-white text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Filtreleri Temizle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<Widget
|
||||
title="Toplam Personel"
|
||||
value={employees?.length || 0}
|
||||
color="blue"
|
||||
icon="FaUsers"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Aktif Personel"
|
||||
value={
|
||||
employees?.filter((e) => e.employeeStatus === EmployeeStatusEnum.Active).length || 0
|
||||
}
|
||||
color="green"
|
||||
icon="FaCheckCircle"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="İzinli Personel"
|
||||
value={
|
||||
employees?.filter((e) => e.employeeStatus === EmployeeStatusEnum.OnLeave).length || 0
|
||||
}
|
||||
color="yellow"
|
||||
icon="FaCalendar"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Yeni İşe Alınanlar"
|
||||
value={
|
||||
employees?.filter((e) => dayjs(e.hireDate).isAfter(dayjs().subtract(30, 'day')))
|
||||
.length || 0
|
||||
}
|
||||
color="purple"
|
||||
icon="FaArrowUp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Employees Display */}
|
||||
{viewMode === 'list' ? (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 text-xs">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Personel Bilgileri
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
İletişim
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Pozisyon / Departman
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
İstihdam Bilgileri
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Maaş
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Durum
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
İşlemler
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{employees?.map((employee) => (
|
||||
<tr key={employee.id} className="hover:bg-gray-50 transition-colors text-xs">
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-10 w-10">
|
||||
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<FaUsers className="h-5 w-5 text-blue-600" />
|
||||
<img
|
||||
src={employee.avatar}
|
||||
alt={employee.fullName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-xs font-medium text-gray-900">{employee.code}</div>
|
||||
<div className="text-xs text-gray-500">{employee.fullName}</div>
|
||||
{employee.badgeNumber && (
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Rozet: {employee.badgeNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center text-xs text-gray-900">
|
||||
<FaEnvelope size={14} className="mr-1" />
|
||||
{employee.email}
|
||||
</div>
|
||||
{employee.phone && (
|
||||
<div className="flex items-center text-xs text-gray-500">
|
||||
<FaPhone size={14} className="mr-1" />
|
||||
{employee.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
{employee.jobPosition?.name}
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-gray-500">
|
||||
<FaBuilding size={14} className="mr-1" />
|
||||
{employee.department?.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{employee.workLocation}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<div className="space-y-1">
|
||||
<div
|
||||
className={classNames(
|
||||
'text-xs font-medium',
|
||||
getEmploymentTypeColor(employee.employmentType),
|
||||
)}
|
||||
>
|
||||
{getEmploymentTypeText(employee.employmentType)}
|
||||
</div>
|
||||
<div className="flex items-center text-xs text-gray-500">
|
||||
<FaCalendar size={14} className="mr-1" />
|
||||
{dayjs(employee.hireDate).format('DD.MM.YYYY')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{dayjs().diff(employee.hireDate, 'year')} yıl deneyim
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<div className="text-xs font-medium text-gray-900">
|
||||
₺{employee.baseSalary.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{employee.payrollGroup}</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||
getEmployeeStatusColor(employee.employeeStatus),
|
||||
)}
|
||||
>
|
||||
{getEmployeeStatusIcon(employee.employeeStatus)}
|
||||
<span className="ml-1">
|
||||
{getEmployeeStatusText(employee.employeeStatus)}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => handleViewEmployee(employee)}
|
||||
className="p-1 text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Detayları Görüntüle"
|
||||
>
|
||||
<FaEye size={16} />
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.hr.employeesEdit.replace(':id', employee.id)}
|
||||
className="p-1 text-gray-600 hover:text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{(!employees || employees.length === 0) && (
|
||||
<div className="text-center py-12">
|
||||
<FaUsers className="mx-auto h-10 w-10 text-gray-400" />
|
||||
<h3 className="mt-2 text-xs font-medium text-gray-900">Personel bulunamadı</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">Yeni personel ekleyerek başlayın.</p>
|
||||
<div className="mt-6">
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.hr.employeesNew}
|
||||
className="inline-flex items-center px-3 py-1 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlus size={16} className="mr-2" />
|
||||
Yeni Personel Ekle
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{employees?.map((employee) => (
|
||||
<div
|
||||
key={employee.id}
|
||||
className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow p-3"
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div className="flex items-start space-x-4 mb-4">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="h-12 w-12 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<span className="text-lg font-medium text-white">
|
||||
{employee.firstName.charAt(0)}
|
||||
{employee.lastName.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-semibold text-gray-900 truncate">
|
||||
{employee.fullName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{employee.code}</p>
|
||||
<span
|
||||
className={classNames(
|
||||
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mt-1',
|
||||
getEmployeeStatusColor(employee.employeeStatus),
|
||||
)}
|
||||
>
|
||||
{getEmployeeStatusIcon(employee.employeeStatus)}
|
||||
<span className="ml-1">{getEmployeeStatusText(employee.employeeStatus)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Content */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<FaBriefcase className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>{employee.jobPosition?.name || 'Pozisyon belirtilmemiş'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<FaBuilding className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>{employee.department?.name || 'Departman belirtilmemiş'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<FaEnvelope className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span className="truncate">{employee.email}</span>
|
||||
</div>
|
||||
|
||||
{employee.phone && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<FaPhone className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>{employee.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<FaCalendar className="w-4 h-4 mr-2 text-gray-400" />
|
||||
<span>İşe Başlama: {dayjs(employee.hireDate).format('DD.MM.YYYY')}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">İstihdam Türü:</span> {employee.employmentType}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Actions */}
|
||||
<div className="flex justify-end space-x-1 mt-2 pt-2 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => handleViewEmployee(employee)}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.hr.employeesEdit.replace(':id', employee.id)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => alert('Performans değerlendirme özelliği yakında eklenecek')}
|
||||
className="p-1 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||||
title="Performans"
|
||||
>
|
||||
<FaAward className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Employee View Modal */}
|
||||
<EmployeeView
|
||||
isOpen={isViewModalOpen}
|
||||
onClose={handleCloseViewModal}
|
||||
employee={selectedEmployee}
|
||||
onEdit={handleEditFromView}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmployeeList
|
||||
|
|
@ -1,497 +0,0 @@
|
|||
import React from "react";
|
||||
import {
|
||||
FaTimes,
|
||||
FaUser,
|
||||
FaIdCard,
|
||||
FaEnvelope,
|
||||
FaPhone,
|
||||
FaCalendar,
|
||||
FaBuilding,
|
||||
FaBriefcase,
|
||||
FaDollarSign,
|
||||
FaMapMarkerAlt,
|
||||
FaUserShield,
|
||||
FaHeartbeat,
|
||||
FaEdit,
|
||||
FaClock,
|
||||
FaBirthdayCake,
|
||||
FaGraduationCap,
|
||||
FaAward,
|
||||
FaHistory,
|
||||
} from "react-icons/fa";
|
||||
import { EmployeeDto } from "../../../types/hr";
|
||||
import {
|
||||
getEmployeeStatusColor,
|
||||
getEmployeeStatusIcon,
|
||||
getEmployeeStatusText,
|
||||
} from "../../../utils/erp";
|
||||
|
||||
interface EmployeeViewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
employee: EmployeeDto | null;
|
||||
onEdit?: (employee: EmployeeDto) => void;
|
||||
}
|
||||
|
||||
const EmployeeViewModal: React.FC<EmployeeViewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
employee,
|
||||
onEdit,
|
||||
}) => {
|
||||
if (!isOpen || !employee) return null;
|
||||
|
||||
const formatDate = (date: Date | string): string => {
|
||||
return new Date(date).toLocaleDateString("tr-TR");
|
||||
};
|
||||
|
||||
const calculateWorkDuration = (hireDate: Date): string => {
|
||||
const now = new Date();
|
||||
const hire = new Date(hireDate);
|
||||
const diffTime = Math.abs(now.getTime() - hire.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
const years = Math.floor(diffDays / 365);
|
||||
const months = Math.floor((diffDays % 365) / 30);
|
||||
|
||||
if (years > 0) {
|
||||
return `${years} yıl ${months} ay`;
|
||||
}
|
||||
return `${months} ay`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-2xl w-full max-w-4xl max-h-[95vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-indigo-700 text-white p-3 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
|
||||
<FaUser className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">{employee.fullName}</h2>
|
||||
<p className="text-blue-100 text-sm">
|
||||
{employee.jobPosition?.name || "Pozisyon Belirtilmemiş"}
|
||||
</p>
|
||||
<p className="text-blue-200 text-xs">{employee.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className={`px-2 py-0.5 rounded-full border flex items-center space-x-1.5 text-xs ${getEmployeeStatusColor(
|
||||
employee.employeeStatus
|
||||
)} bg-opacity-90`}
|
||||
>
|
||||
{getEmployeeStatusIcon(employee.employeeStatus)}
|
||||
<span className="font-medium text-xs">
|
||||
{getEmployeeStatusText(employee.employeeStatus)}
|
||||
</span>
|
||||
</div>
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={() => onEdit(employee)}
|
||||
className="bg-white bg-opacity-20 hover:bg-opacity-30 text-white px-2 py-1 text-xs rounded-md flex items-center space-x-1.5 transition-all"
|
||||
>
|
||||
<FaEdit className="w-3 h-3" />
|
||||
<span>Düzenle</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:bg-white hover:bg-opacity-20 p-1 rounded-lg transition-all"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-3 overflow-y-auto flex-grow">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
|
||||
{/* Left Column - Personal Info */}
|
||||
<div className="space-y-3">
|
||||
{/* Kişisel Bilgiler */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaIdCard className="w-4 h-4 text-blue-600 mr-2" />
|
||||
Kişisel Bilgiler
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaBirthdayCake className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Doğum Tarihi</p>
|
||||
<p className="font-medium text-sm">
|
||||
{formatDate(employee.birthDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaUser className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Cinsiyet</p>
|
||||
<p className="font-medium text-sm">{employee.gender}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaHeartbeat className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Medeni Durum</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.maritalStatus}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaIdCard className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">TC Kimlik No</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.nationalId}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* İletişim Bilgileri */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaPhone className="w-4 h-4 text-green-600 mr-2" />
|
||||
İletişim Bilgileri
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaEnvelope className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">E-posta</p>
|
||||
<p className="font-medium text-sm text-blue-600">
|
||||
{employee.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{employee.phone && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaPhone className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">İş Telefonu</p>
|
||||
<p className="font-medium text-sm">{employee.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{employee.personalPhone && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaPhone className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Kişisel Telefon</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.personalPhone}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{employee.address && (
|
||||
<div className="flex items-start space-x-2">
|
||||
<FaMapMarkerAlt className="w-3 h-3 text-gray-500 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Adres</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.address.street}
|
||||
<br />
|
||||
{employee.address.city}, {employee.address.state}
|
||||
<br />
|
||||
{employee.address.postalCode},{" "}
|
||||
{employee.address.country}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Acil Durum İletişim */}
|
||||
{employee.emergencyContact && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaUserShield className="w-4 h-4 text-red-600 mr-2" />
|
||||
Acil Durum İletişim
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">İsim</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.emergencyContact.name}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Telefon</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.emergencyContact.phone}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Yakınlık</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.emergencyContact.relationship}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Middle Column - Work Info */}
|
||||
<div className="space-y-3">
|
||||
{/* İş Bilgileri */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaBriefcase className="w-4 h-4 text-purple-600 mr-2" />
|
||||
İş Bilgileri
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaBuilding className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Departman</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.department?.name || "Belirtilmemiş"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaGraduationCap className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Pozisyon</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.jobPosition?.name || "Belirtilmemiş"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaCalendar className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">
|
||||
İşe Başlama Tarihi
|
||||
</p>
|
||||
<p className="font-medium text-sm">
|
||||
{formatDate(employee.hireDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaHistory className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Çalışma Süresi</p>
|
||||
<p className="font-medium text-sm">
|
||||
{calculateWorkDuration(employee.hireDate)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaBriefcase className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Çalışma Türü</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.employmentType}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{employee.workLocation && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaMapMarkerAlt className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">
|
||||
Çalışma Lokasyonu
|
||||
</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.workLocation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{employee.badgeNumber && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<FaIdCard className="w-3 h-3 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Rozet Numarası</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.badgeNumber}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yönetici Bilgileri */}
|
||||
{employee.manager && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaUserShield className="w-4 h-4 text-indigo-600 mr-2" />
|
||||
Yönetici
|
||||
</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<FaUser className="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-sm text-gray-900">
|
||||
{employee.manager.fullName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">
|
||||
{employee.manager.jobPosition?.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{employee.manager.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Performans ve Eğitimler */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaAward className="w-4 h-4 text-amber-600 mr-2" />
|
||||
Performans & Gelişim
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="text-center p-1.5 bg-white rounded-md">
|
||||
<p className="text-xl font-bold text-amber-600">
|
||||
{employee.evaluations?.length || 0}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">Değerlendirme</p>
|
||||
</div>
|
||||
<div className="text-center p-1.5 bg-white rounded-md">
|
||||
<p className="text-xl font-bold text-emerald-600">
|
||||
{employee.trainings?.length || 0}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600">Eğitim</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Financial & Additional Info */}
|
||||
<div className="space-y-3">
|
||||
{/* Maaş Bilgileri */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaDollarSign className="w-4 h-4 text-emerald-600 mr-2" />
|
||||
Maaş Bilgileri
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Temel Maaş</p>
|
||||
<p className="text-xl font-bold text-emerald-600">
|
||||
{employee.baseSalary?.toLocaleString()}{" "}
|
||||
{employee.currency || "TRY"}
|
||||
</p>
|
||||
</div>
|
||||
{employee.payrollGroup && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Bordro Grubu</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.payrollGroup}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banka Bilgileri */}
|
||||
{employee.bankAccount && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaDollarSign className="w-4 h-4 text-slate-600 mr-2" />
|
||||
Banka Bilgileri
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Banka Adı</p>
|
||||
<p className="font-medium text-sm">
|
||||
{employee.bankAccount.bankName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">Hesap Numarası</p>
|
||||
<p className="font-mono text-xs">
|
||||
{employee.bankAccount.accountNumber}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">IBAN</p>
|
||||
<p className="font-mono text-xs">
|
||||
{employee.bankAccount.iban}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* İzin Durumu */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaCalendar className="w-4 h-4 text-sky-600 mr-2" />
|
||||
İzin Durumu
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="bg-white p-1.5 rounded-md flex justify-between items-center">
|
||||
<span className="text-xs text-gray-600">Toplam İzin</span>
|
||||
<span className="font-bold text-sm text-sky-600">
|
||||
{employee.leaves?.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-white p-1.5 rounded-md flex justify-between items-center">
|
||||
<span className="text-xs text-gray-600">Kalan İzin</span>
|
||||
<span className="font-bold text-sm text-emerald-600">
|
||||
15 gün
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sistem Bilgileri */}
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2 flex items-center">
|
||||
<FaClock className="w-4 h-4 text-gray-600 mr-2" />
|
||||
Sistem Bilgileri
|
||||
</h3>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div>
|
||||
<p className="text-gray-600">Oluşturulma Tarihi</p>
|
||||
<p className="font-medium text-gray-800">
|
||||
{formatDate(employee.creationTime)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-600">Son Güncelleme</p>
|
||||
<p className="font-medium text-gray-800">
|
||||
{formatDate(employee.lastModificationTime)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="bg-gray-50 px-3 py-2 border-t flex-shrink-0">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeViewModal;
|
||||
|
|
@ -1,454 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { FaPlus, FaEdit, FaEye, FaTh, FaList, FaSearch } from 'react-icons/fa'
|
||||
import { EmploymentTypeEnum } from '../../../types/hr'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { mockEmployeeTypes } from '../../../mocks/mockEmployeeTypes'
|
||||
import Widget from '../../../components/common/Widget'
|
||||
import { getEmploymentTypeColor, getEmploymentTypeText } from '../../../utils/erp'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const EmploymentTypes: React.FC = () => {
|
||||
const [viewMode, setViewMode] = useState<'card' | 'list'>('list')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedEmploymentType, setSelectedEmploymentType] = useState<EmploymentTypeEnum | null>(
|
||||
null,
|
||||
)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [modalMode, setModalMode] = useState<'new' | 'edit' | 'view'>('view')
|
||||
|
||||
const handleNewEmploymentType = () => {
|
||||
setModalMode('new')
|
||||
setSelectedEmploymentType(null)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleEditEmploymentType = (type: EmploymentTypeEnum) => {
|
||||
setModalMode('edit')
|
||||
setSelectedEmploymentType(type)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleViewEmploymentType = (type: EmploymentTypeEnum) => {
|
||||
setModalMode('view')
|
||||
setSelectedEmploymentType(type)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
// Filter employee types based on search term
|
||||
const filteredEmployeeTypes = mockEmployeeTypes.filter((employeeType) =>
|
||||
getEmploymentTypeText(employeeType.name as EmploymentTypeEnum)
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">İstihdam Türleri</h2>
|
||||
<p className="text-gray-600">Personel istihdam türleri ve dağılımı</p>
|
||||
</div>
|
||||
|
||||
{/* New Button - Visible on larger screens */}
|
||||
<div className="hidden sm:flex gap-2">
|
||||
<button
|
||||
onClick={handleNewEmploymentType}
|
||||
className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
<span>Yeni</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search and Controls */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1">
|
||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="İstihdam türü ara..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 pr-4 py-1.5 text-sm border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex justify-between sm:justify-end items-center gap-3">
|
||||
{/* View Toggle */}
|
||||
<div className="flex bg-gray-100 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('card')}
|
||||
className={`p-2 rounded ${
|
||||
viewMode === 'card'
|
||||
? 'bg-white shadow text-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
title="Kart Görünümü"
|
||||
>
|
||||
<FaTh className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded ${
|
||||
viewMode === 'list'
|
||||
? 'bg-white shadow text-blue-600'
|
||||
: 'text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
title="Liste Görünümü"
|
||||
>
|
||||
<FaList className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* New Button - Visible on mobile */}
|
||||
<div className="sm:hidden">
|
||||
<button
|
||||
onClick={handleNewEmploymentType}
|
||||
className="flex items-center gap-2 bg-blue-600 text-white px-3 py-1.5 text-sm rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
<span className="text-sm">Yeni</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Stats */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Widget
|
||||
title="Toplam Personel"
|
||||
value={mockEmployees.length}
|
||||
color="blue"
|
||||
icon="FaUsers"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Tam Zamanlı"
|
||||
value={
|
||||
mockEmployees.filter((a) => a.employmentType === EmploymentTypeEnum.FullTime)
|
||||
.length || 0
|
||||
}
|
||||
color="green"
|
||||
icon="FaClock"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="Sözleşmeli"
|
||||
value={
|
||||
mockEmployees.filter((a) => a.employmentType === EmploymentTypeEnum.Contract)
|
||||
.length || 0
|
||||
}
|
||||
color="orange"
|
||||
icon="FaFileAlt"
|
||||
/>
|
||||
|
||||
<Widget
|
||||
title="İstihdam Türü"
|
||||
value={mockEmployeeTypes.length}
|
||||
color="purple"
|
||||
icon="FaChartBar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Employment Type Distribution */}
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
{viewMode === 'card' ? (
|
||||
// Card View
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
{filteredEmployeeTypes.map((a) => {
|
||||
const type = a.name as EmploymentTypeEnum // veya a’dan türet
|
||||
const count = a.count || 0
|
||||
const total = mockEmployees.length
|
||||
const percentage = total > 0 ? (count / total) * 100 : 0
|
||||
|
||||
return (
|
||||
<div
|
||||
key={a.id}
|
||||
className="border rounded-lg p-3 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-4 h-4 rounded-full ${getEmploymentTypeColor(type)}`} />
|
||||
<h4 className="font-medium text-gray-900 text-sm">
|
||||
{getEmploymentTypeText(type)}
|
||||
</h4>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-xs text-gray-600">Personel Sayısı</span>
|
||||
<span className="text-xs font-medium">{count}</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getEmploymentTypeColor(type)}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 mt-2 pt-2 border-t">
|
||||
<button
|
||||
onClick={() => handleViewEmploymentType(type)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-50 text-blue-600 rounded-md hover:bg-blue-100 transition-colors"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Görüntüle</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditEmploymentType(type)}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-yellow-50 text-yellow-600 rounded-md hover:bg-yellow-100 transition-colors"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-3 h-3" />
|
||||
<span className="hidden sm:inline">Düzenle</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// List View
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr className="text-sm">
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
İstihdam Türü
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Personel Sayısı
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Yüzde
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
İşlemler
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredEmployeeTypes.map((a) => {
|
||||
const type = a.name as EmploymentTypeEnum
|
||||
const count = a.count || 0
|
||||
const total = mockEmployees.length
|
||||
const percentage = total > 0 ? (count / total) * 100 : 0
|
||||
return (
|
||||
<tr key={type} className="hover:bg-gray-50">
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full mr-3 ${getEmploymentTypeColor(
|
||||
type,
|
||||
)}`}
|
||||
/>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{getEmploymentTypeText(type)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
||||
{count}
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="w-16 bg-gray-200 rounded-full h-2 mr-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getEmploymentTypeColor(type)}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-gray-900">{percentage.toFixed(1)}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleViewEmploymentType(type)}
|
||||
className="text-blue-600 hover:text-blue-900 p-1"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditEmploymentType(type)}
|
||||
className="text-yellow-600 hover:text-yellow-900 p-1"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-lg p-4 w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-base font-semibold">
|
||||
{modalMode === 'new' && 'Yeni İstihdam Türü'}
|
||||
{modalMode === 'edit' && 'İstihdam Türünü Düzenle'}
|
||||
{modalMode === 'view' && 'İstihdam Türü Detayları'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600 p-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{modalMode === 'new' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
İstihdam Türü Adı
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
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"
|
||||
placeholder="Örn: Tam Zamanlı"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Açıklama</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
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"
|
||||
placeholder="İstihdam türü açıklaması..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalMode === 'edit' && selectedEmploymentType && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
İstihdam Türü Adı
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={getEmploymentTypeText(selectedEmploymentType)}
|
||||
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-2">Açıklama</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
defaultValue="Bu istihdam türüne ait açıklama..."
|
||||
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>
|
||||
)}
|
||||
|
||||
{modalMode === 'view' && selectedEmploymentType && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">İstihdam Türü:</p>
|
||||
<p className="font-medium text-base">
|
||||
{getEmploymentTypeText(selectedEmploymentType)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Toplam Personel Sayısı:</p>
|
||||
<p className="font-medium">
|
||||
{
|
||||
mockEmployees.filter((emp) => emp.employmentType === selectedEmploymentType)
|
||||
.length
|
||||
}{' '}
|
||||
kişi
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Renk Kodu:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full ${getEmploymentTypeColor(
|
||||
selectedEmploymentType,
|
||||
)}`}
|
||||
/>
|
||||
<span className="text-sm">
|
||||
{getEmploymentTypeColor(selectedEmploymentType)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Bu istihdam türündeki personeller:</p>
|
||||
<div className="max-h-32 overflow-y-auto bg-gray-50 rounded p-2">
|
||||
{mockEmployees
|
||||
.filter((emp) => emp.employmentType === selectedEmploymentType)
|
||||
.slice(0, 10)
|
||||
.map((emp) => (
|
||||
<div
|
||||
key={emp.id}
|
||||
className="text-sm py-1 border-b border-gray-200 last:border-0"
|
||||
>
|
||||
{emp.fullName} - {emp.department?.name}
|
||||
</div>
|
||||
))}
|
||||
{mockEmployees.filter((emp) => emp.employmentType === selectedEmploymentType)
|
||||
.length > 10 && (
|
||||
<div className="text-xs text-gray-500 pt-2">
|
||||
+
|
||||
{mockEmployees.filter(
|
||||
(emp) => emp.employmentType === selectedEmploymentType,
|
||||
).length - 10}{' '}
|
||||
kişi daha...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-end gap-2 mt-4">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-3 py-1.5 text-sm text-gray-600 border rounded-md hover:bg-gray-50 order-2 sm:order-1"
|
||||
>
|
||||
{modalMode === 'view' ? 'Kapat' : 'İptal'}
|
||||
</button>
|
||||
{modalMode !== 'view' && (
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 order-1 sm:order-2"
|
||||
>
|
||||
{modalMode === 'new' ? 'Oluştur' : 'Güncelle'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmploymentTypes
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { FaSave, FaTimes, FaPlus, FaTrash } from 'react-icons/fa'
|
||||
import { JobPositionDto, JobLevelEnum } from '../../../types/hr'
|
||||
import { mockDepartments } from '../../../mocks/mockDepartments'
|
||||
import { getJobLevelText } from '@/utils/erp'
|
||||
import { mockCurrencies } from '@/mocks/mockCurrencies'
|
||||
|
||||
interface JobPositionFormModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (position: Partial<JobPositionDto>) => void
|
||||
position?: JobPositionDto
|
||||
title: string
|
||||
}
|
||||
|
||||
const JobPositionFormModal: React.FC<JobPositionFormModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
position,
|
||||
title,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState({
|
||||
positionCode: '',
|
||||
title: '',
|
||||
description: '',
|
||||
departmentId: '',
|
||||
level: JobLevelEnum.Entry,
|
||||
minSalary: 0,
|
||||
maxSalary: 0,
|
||||
currency: 'TRY',
|
||||
requiredSkills: [] as string[],
|
||||
responsibilities: [] as string[],
|
||||
qualifications: [] as string[],
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [skillInput, setSkillInput] = useState('')
|
||||
const [responsibilityInput, setResponsibilityInput] = useState('')
|
||||
const [qualificationInput, setQualificationInput] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (position) {
|
||||
setFormData({
|
||||
positionCode: position.code,
|
||||
title: position.name,
|
||||
description: position.description || '',
|
||||
departmentId: position.departmentId,
|
||||
level: position.level,
|
||||
minSalary: position.minSalary,
|
||||
maxSalary: position.maxSalary,
|
||||
currency: position.currency,
|
||||
requiredSkills: [...(position.requiredSkills || [])],
|
||||
responsibilities: [...(position.responsibilities || [])],
|
||||
qualifications: [...(position.qualifications || [])],
|
||||
isActive: position.isActive,
|
||||
})
|
||||
} else {
|
||||
setFormData({
|
||||
positionCode: '',
|
||||
title: '',
|
||||
description: '',
|
||||
departmentId: '',
|
||||
level: JobLevelEnum.Entry,
|
||||
minSalary: 0,
|
||||
maxSalary: 0,
|
||||
currency: 'TRY',
|
||||
requiredSkills: [],
|
||||
responsibilities: [],
|
||||
qualifications: [],
|
||||
isActive: true,
|
||||
})
|
||||
}
|
||||
setErrors({})
|
||||
setSkillInput('')
|
||||
setResponsibilityInput('')
|
||||
setQualificationInput('')
|
||||
}, [position, isOpen])
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
if (!formData.positionCode.trim()) {
|
||||
newErrors.positionCode = 'Pozisyon kodu zorunludur'
|
||||
}
|
||||
|
||||
if (!formData.title.trim()) {
|
||||
newErrors.title = 'Pozisyon adı zorunludur'
|
||||
}
|
||||
|
||||
if (!formData.departmentId) {
|
||||
newErrors.departmentId = 'Departman seçimi zorunludur'
|
||||
}
|
||||
|
||||
if (formData.minSalary < 0) {
|
||||
newErrors.minSalary = 'Minimum maaş negatif olamaz'
|
||||
}
|
||||
|
||||
if (formData.maxSalary < 0) {
|
||||
newErrors.maxSalary = 'Maksimum maaş negatif olamaz'
|
||||
}
|
||||
|
||||
if (formData.maxSalary < formData.minSalary) {
|
||||
newErrors.maxSalary = 'Maksimum maaş minimum maaştan düşük olamaz'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (validateForm()) {
|
||||
onSave(formData)
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (field: string, value: string | number | boolean | JobLevelEnum) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
const addSkill = () => {
|
||||
if (skillInput.trim() && !formData.requiredSkills.includes(skillInput.trim())) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
requiredSkills: [...prev.requiredSkills, skillInput.trim()],
|
||||
}))
|
||||
setSkillInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeSkill = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
requiredSkills: prev.requiredSkills.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
const addResponsibility = () => {
|
||||
if (
|
||||
responsibilityInput.trim() &&
|
||||
!formData.responsibilities.includes(responsibilityInput.trim())
|
||||
) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
responsibilities: [...prev.responsibilities, responsibilityInput.trim()],
|
||||
}))
|
||||
setResponsibilityInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeResponsibility = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
responsibilities: prev.responsibilities.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
const addQualification = () => {
|
||||
if (qualificationInput.trim() && !formData.qualifications.includes(qualificationInput.trim())) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
qualifications: [...prev.qualifications, qualificationInput.trim()],
|
||||
}))
|
||||
setQualificationInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeQualification = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
qualifications: prev.qualifications.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-3 border-b">
|
||||
<h2 className="text-xl font-bold text-gray-900">{title}</h2>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded-full">
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-3 space-y-3">
|
||||
{/* Basic Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Pozisyon Kodu *
|
||||
</label>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={formData.positionCode}
|
||||
onChange={(e) => handleInputChange('positionCode', e.target.value)}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.positionCode ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="Örn: DEV-001"
|
||||
/>
|
||||
{errors.positionCode && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.positionCode}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Pozisyon Adı *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleInputChange('title', e.target.value)}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.title ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="Örn: Software Developer"
|
||||
/>
|
||||
{errors.title && <p className="text-red-500 text-sm mt-1">{errors.title}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Açıklama</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Pozisyon açıklaması..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Departman *</label>
|
||||
<select
|
||||
value={formData.departmentId}
|
||||
onChange={(e) => handleInputChange('departmentId', e.target.value)}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.departmentId ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<option value="">Departman seçin</option>
|
||||
{mockDepartments.map((dept) => (
|
||||
<option key={dept.id} value={dept.id}>
|
||||
{dept.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.departmentId && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.departmentId}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Seviye</label>
|
||||
<select
|
||||
value={formData.level}
|
||||
onChange={(e) => handleInputChange('level', e.target.value as JobLevelEnum)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{Object.values(JobLevelEnum).map((level) => (
|
||||
<option key={level} value={level}>
|
||||
{getJobLevelText(level)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Salary Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Minimum Maaş *</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.minSalary}
|
||||
onChange={(e) => handleInputChange('minSalary', Number(e.target.value))}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.minSalary ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
min="0"
|
||||
/>
|
||||
{errors.minSalary && <p className="text-red-500 text-sm mt-1">{errors.minSalary}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Maksimum Maaş *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.maxSalary}
|
||||
onChange={(e) => handleInputChange('maxSalary', Number(e.target.value))}
|
||||
className={`w-full px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
||||
errors.maxSalary ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
min="0"
|
||||
/>
|
||||
{errors.maxSalary && <p className="text-red-500 text-sm mt-1">{errors.maxSalary}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Para Birimi</label>
|
||||
<select
|
||||
value={formData.currency}
|
||||
onChange={(e) => handleInputChange('currency', e.target.value)}
|
||||
className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{mockCurrencies.map((currency) => (
|
||||
<option key={currency.value} value={currency.value}>
|
||||
{currency.value} - {currency.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Required Skills */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||
Gerekli Yetenekler
|
||||
</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={skillInput}
|
||||
onChange={(e) => setSkillInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addSkill())}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Yetenek ekleyin..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addSkill}
|
||||
className="px-2 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.requiredSkills.map((skill, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-700 rounded-full text-xs"
|
||||
>
|
||||
{skill}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSkill(index)}
|
||||
className="hover:text-blue-900"
|
||||
>
|
||||
<FaTrash className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Responsibilities */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Sorumluluklar</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={responsibilityInput}
|
||||
onChange={(e) => setResponsibilityInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addResponsibility())}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Sorumluluk ekleyin..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addResponsibility}
|
||||
className="px-2 py-1.5 text-sm bg-green-600 text-white rounded-md hover:bg-green-700"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{formData.responsibilities.map((responsibility, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-2 bg-green-50 rounded-md text-sm"
|
||||
>
|
||||
<span className="text-green-800 text-sm">{responsibility}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeResponsibility(index)}
|
||||
className="text-green-600 hover:text-green-800"
|
||||
>
|
||||
<FaTrash className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Qualifications */}
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Nitelikler</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={qualificationInput}
|
||||
onChange={(e) => setQualificationInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addQualification())}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Nitelik ekleyin..."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addQualification}
|
||||
className="px-2 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
|
||||
>
|
||||
<FaPlus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{formData.qualifications.map((qualification, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-2 bg-purple-50 rounded-md text-sm"
|
||||
>
|
||||
<span className="text-purple-800 text-sm">{qualification}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeQualification(index)}
|
||||
className="text-purple-600 hover:text-purple-800"
|
||||
>
|
||||
<FaTrash className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">
|
||||
Aktif pozisyon
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex gap-2 pt-3 border-t">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<FaSave className="w-4 h-4" />
|
||||
Kaydet
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
||||
>
|
||||
<FaTimes className="w-4 h-4" />
|
||||
İptal
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobPositionFormModal
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
import React from "react";
|
||||
import {
|
||||
FaTimes,
|
||||
FaBriefcase,
|
||||
FaBuilding,
|
||||
FaUsers,
|
||||
FaDollarSign,
|
||||
FaClock,
|
||||
} from "react-icons/fa";
|
||||
import { JobPositionDto } from "../../../types/hr";
|
||||
import { getJobLevelColor, getJobLevelText } from "../../../utils/erp";
|
||||
|
||||
interface JobPositionViewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
position: JobPositionDto | null | undefined;
|
||||
}
|
||||
|
||||
const JobPositionViewModal: React.FC<JobPositionViewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
position,
|
||||
}) => {
|
||||
if (!isOpen || !position) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-3 border-b">
|
||||
<div className="flex items-center gap-3">
|
||||
<FaBriefcase className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{position.name}
|
||||
</h2>
|
||||
<p className="text-gray-600">{position.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-100 rounded-full"
|
||||
>
|
||||
<FaTimes className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Status and Level */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getJobLevelColor(
|
||||
position.level
|
||||
)}`}
|
||||
>
|
||||
{getJobLevelText(position.level)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
position.isActive
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{position.isActive ? "Aktif" : "Pasif"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
||||
Temel Bilgiler
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<FaBuilding className="w-4 h-4 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
Departman
|
||||
</p>
|
||||
<p className="text-gray-900">
|
||||
{position.department?.name || "Belirtilmemiş"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<FaUsers className="w-4 h-4 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
Mevcut Personel
|
||||
</p>
|
||||
<p className="text-gray-900">
|
||||
{position.employees?.length || 0} kişi
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
Maaş Aralığı
|
||||
</p>
|
||||
<p className="text-gray-900">
|
||||
{position.minSalary.toLocaleString()} -{" "}
|
||||
{position.maxSalary.toLocaleString()}{" "}
|
||||
{position.currency}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<FaClock className="w-4 h-4 text-gray-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">
|
||||
Oluşturulma Tarihi
|
||||
</p>
|
||||
<p className="text-gray-900">
|
||||
{position.creationTime.toLocaleDateString("tr-TR")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
||||
Açıklama
|
||||
</h3>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
{position.description || "Açıklama bulunmuyor."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills */}
|
||||
{position.requiredSkills && position.requiredSkills.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Gerekli Yetenekler
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{position.requiredSkills.map((skill, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded-full"
|
||||
>
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Responsibilities */}
|
||||
{position.responsibilities &&
|
||||
position.responsibilities.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Sorumluluklar
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{position.responsibilities.map((responsibility, index) => (
|
||||
<div key={index} className="flex items-start gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 flex-shrink-0" />
|
||||
<p className="text-gray-700">{responsibility}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Qualifications */}
|
||||
{position.qualifications && position.qualifications.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Nitelikler
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{position.qualifications.map((qualification, index) => (
|
||||
<div key={index} className="flex items-start gap-2">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full mt-2 flex-shrink-0" />
|
||||
<p className="text-gray-700">{qualification}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Employees in this position */}
|
||||
{position.employees && position.employees.length > 0 && (
|
||||
<div className="text-sm">
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">
|
||||
Bu Pozisyondaki Personeller
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{position.employees.map((employee, index) => (
|
||||
<div key={index} className="p-1.5 bg-gray-50 rounded-md">
|
||||
<p className="font-medium text-gray-900">
|
||||
{employee.fullName}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">{employee.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t bg-gray-50">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full sm:w-auto px-3 py-1.5 text-xs bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobPositionViewModal;
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import {
|
||||
FaBriefcase,
|
||||
FaPlus,
|
||||
FaEdit,
|
||||
FaTrash,
|
||||
FaUsers,
|
||||
FaDollarSign,
|
||||
FaBuilding,
|
||||
FaEye,
|
||||
FaTh,
|
||||
FaList,
|
||||
} from 'react-icons/fa'
|
||||
import { JobPositionDto, JobLevelEnum } from '../../../types/hr'
|
||||
import DataTable, { Column } from '../../../components/common/DataTable'
|
||||
import { mockJobPositions } from '../../../mocks/mockJobPositions'
|
||||
import JobPositionFormModal from './JobPositionFormModal'
|
||||
import JobPositionViewModal from './JobPositionViewModal'
|
||||
import { getJobLevelColor, getJobLevelText } from '../../../utils/erp'
|
||||
import { Container } from '@/components/shared'
|
||||
|
||||
const JobPositions: React.FC = () => {
|
||||
const [positions, setPositions] = useState<JobPositionDto[]>(mockJobPositions)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedLevel, setSelectedLevel] = useState<string>('all')
|
||||
const [selectedDepartment, setSelectedDepartment] = useState<string>('all')
|
||||
const [viewMode, setViewMode] = useState<'list' | 'card'>('list')
|
||||
|
||||
// Modal states
|
||||
const [isFormModalOpen, setIsFormModalOpen] = useState(false)
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
||||
const [selectedPosition, setSelectedPosition] = useState<JobPositionDto | undefined>(undefined)
|
||||
const [modalTitle, setModalTitle] = useState('')
|
||||
|
||||
const handleAdd = () => {
|
||||
setSelectedPosition(undefined)
|
||||
setModalTitle('Yeni İş Pozisyonu')
|
||||
setIsFormModalOpen(true)
|
||||
}
|
||||
|
||||
const handleEdit = (position: JobPositionDto) => {
|
||||
setSelectedPosition(position)
|
||||
setModalTitle('İş Pozisyonu Düzenle')
|
||||
setIsFormModalOpen(true)
|
||||
}
|
||||
|
||||
const handleView = (position: JobPositionDto) => {
|
||||
setSelectedPosition(position)
|
||||
setIsViewModalOpen(true)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (window.confirm('Bu pozisyonu silmek istediğinizden emin misiniz?')) {
|
||||
setPositions(positions.filter((p) => p.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavePosition = (positionData: Partial<JobPositionDto>) => {
|
||||
if (selectedPosition) {
|
||||
// Edit existing position
|
||||
const updatedPosition = {
|
||||
...selectedPosition,
|
||||
...positionData,
|
||||
lastModificationTime: new Date(),
|
||||
}
|
||||
setPositions(positions.map((p) => (p.id === selectedPosition.id ? updatedPosition : p)))
|
||||
} else {
|
||||
// Add new position
|
||||
const newPosition: JobPositionDto = {
|
||||
id: `jp-${Date.now()}`,
|
||||
...positionData,
|
||||
employees: [],
|
||||
creationTime: new Date(),
|
||||
lastModificationTime: new Date(),
|
||||
} as JobPositionDto
|
||||
setPositions([...positions, newPosition])
|
||||
}
|
||||
}
|
||||
|
||||
const closeFormModal = () => {
|
||||
setIsFormModalOpen(false)
|
||||
setSelectedPosition(undefined)
|
||||
setModalTitle('')
|
||||
}
|
||||
|
||||
const closeViewModal = () => {
|
||||
setIsViewModalOpen(false)
|
||||
setSelectedPosition(undefined)
|
||||
}
|
||||
|
||||
const filteredPositions = positions.filter((position) => {
|
||||
if (
|
||||
searchTerm &&
|
||||
!position.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||
!position.code.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (selectedLevel !== 'all' && position.level !== selectedLevel) {
|
||||
return false
|
||||
}
|
||||
if (selectedDepartment !== 'all' && position.department?.id !== selectedDepartment) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Card component for individual position
|
||||
const PositionCard: React.FC<{ position: JobPositionDto }> = ({ position }) => (
|
||||
<div className="bg-white rounded-lg shadow-sm border hover:shadow-md transition-shadow p-4">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-base font-semibold text-gray-900">{position.name}</h3>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getJobLevelColor(
|
||||
position.level,
|
||||
)}`}
|
||||
>
|
||||
{getJobLevelText(position.level)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">{position.code}</p>
|
||||
<p
|
||||
className="text-sm text-gray-700 overflow-hidden"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{position.description}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
position.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{position.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<FaBuilding className="w-4 h-4" />
|
||||
<span>{position.department?.name || 'Departman belirtilmemiş'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<FaUsers className="w-4 h-4" />
|
||||
<span>{position.employees?.length || 0} personel</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<FaDollarSign className="w-4 h-4" />
|
||||
<span>
|
||||
₺{position.minSalary.toLocaleString()} - ₺{position.maxSalary.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-gray-500 mb-2">Gerekli Yetenekler</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{position.requiredSkills?.slice(0, 4).map((skill, index) => (
|
||||
<span key={index} className="px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
{position.requiredSkills?.length > 4 && (
|
||||
<span className="text-xs text-gray-500 px-2 py-1">
|
||||
+{position.requiredSkills.length - 4} daha
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5">
|
||||
<button
|
||||
onClick={() => handleView(position)}
|
||||
className="flex-1 flex items-center justify-center gap-1 px-2 py-1.5 text-xs text-green-700 bg-green-50 hover:bg-green-100 rounded-md transition-colors"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
Görüntüle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(position)}
|
||||
className="flex-1 flex items-center justify-center gap-1 px-2 py-1.5 text-xs text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-md transition-colors"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
Düzenle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(position.id)}
|
||||
className="px-2 py-1.5 text-xs text-red-700 bg-red-50 hover:bg-red-100 rounded-md transition-colors"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const columns: Column<JobPositionDto>[] = [
|
||||
{
|
||||
key: 'code',
|
||||
header: 'Pozisyon Kodu',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
header: 'Pozisyon Adı',
|
||||
sortable: true,
|
||||
render: (position: JobPositionDto) => (
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{position.name}</div>
|
||||
<div className="text-sm text-gray-500 truncate max-w-xs">{position.description}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'department',
|
||||
header: 'Departman',
|
||||
render: (position: JobPositionDto) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<FaBuilding className="w-4 h-4 text-gray-500" />
|
||||
<span>{position.department?.name || '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'level',
|
||||
header: 'Seviye',
|
||||
render: (position: JobPositionDto) => (
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${getJobLevelColor(
|
||||
position.level,
|
||||
)}`}
|
||||
>
|
||||
{getJobLevelText(position.level)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'employeeCount',
|
||||
header: 'Personel Sayısı',
|
||||
render: (position: JobPositionDto) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaUsers className="w-4 h-4 text-gray-500" />
|
||||
<span>{position.employees?.length || 0}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'salary',
|
||||
header: 'Maaş Aralığı',
|
||||
render: (position: JobPositionDto) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<FaDollarSign className="w-4 h-4 text-gray-500" />
|
||||
<div className="text-sm">
|
||||
<div>₺{position.minSalary.toLocaleString()}</div>
|
||||
<div className="text-gray-500">₺{position.maxSalary.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'skills',
|
||||
header: 'Gerekli Yetenekler',
|
||||
render: (position: JobPositionDto) => (
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{position.requiredSkills?.slice(0, 3).map((skill, index) => (
|
||||
<span key={index} className="px-2 py-1 text-xs bg-blue-50 text-blue-700 rounded">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
{position.requiredSkills?.length > 3 && (
|
||||
<span className="text-xs text-gray-500">
|
||||
+{position.requiredSkills.length - 3} daha
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Durum',
|
||||
render: (position: JobPositionDto) => (
|
||||
<span
|
||||
className={`px-2 py-1 text-xs font-medium rounded-full ${
|
||||
position.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{position.isActive ? 'Aktif' : 'Pasif'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'İşlemler',
|
||||
render: (position: JobPositionDto) => (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleView(position)}
|
||||
className="p-1 text-green-600 hover:bg-green-50 rounded"
|
||||
title="Görüntüle"
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEdit(position)}
|
||||
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
||||
title="Düzenle"
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(position.id)}
|
||||
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
||||
title="Sil"
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// Get unique departments for filter
|
||||
const departments = [...new Set(positions.map((p) => p.department).filter(Boolean))]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="space-y-2">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">İş Pozisyonları</h2>
|
||||
<p className="text-gray-600">Şirket pozisyonları ve iş tanımları yönetimi</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* View Toggle */}
|
||||
<div className="flex bg-gray-100 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
viewMode === 'list'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FaList className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('card')}
|
||||
className={`flex items-center gap-2 px-2 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
viewMode === 'card'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<FaTh className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
<span className="hidden sm:inline">Yeni Pozisyon</span>
|
||||
<span className="sm:hidden">Yeni</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<FaBriefcase className="w-6 h-6 text-blue-600" />
|
||||
<div className="ml-3">
|
||||
<p className="text-xs font-medium text-gray-600">Toplam Pozisyon</p>
|
||||
<p className="text-xl font-bold text-gray-900">{positions.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<FaUsers className="w-6 h-6 text-green-600" />
|
||||
<div className="ml-3">
|
||||
<p className="text-xs font-medium text-gray-600">Dolu Pozisyonlar</p>
|
||||
<p className="text-xl font-bold text-gray-900">
|
||||
{positions.filter((p) => p.employees && p.employees.length > 0).length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<FaBriefcase className="w-6 h-6 text-orange-600" />
|
||||
<div className="ml-3">
|
||||
<p className="text-xs font-medium text-gray-600">Boş Pozisyonlar</p>
|
||||
<p className="text-xl font-bold text-gray-900">
|
||||
{positions.filter((p) => !p.employees || p.employees.length === 0).length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<FaBuilding className="w-6 h-6 text-purple-600" />
|
||||
<div className="ml-3">
|
||||
<p className="text-xs font-medium text-gray-600">Departman Sayısı</p>
|
||||
<p className="text-xl font-bold text-gray-900">{departments.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 items-stretch sm:items-center">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pozisyon adı veya kodu ara..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(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="flex flex-col sm:flex-row gap-3">
|
||||
<select
|
||||
value={selectedLevel}
|
||||
onChange={(e) => setSelectedLevel(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[160px]"
|
||||
>
|
||||
<option value="all">Tüm Seviyeler</option>
|
||||
{Object.values(JobLevelEnum).map((level) => (
|
||||
<option key={level} value={level}>
|
||||
{getJobLevelText(level)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={selectedDepartment}
|
||||
onChange={(e) => setSelectedDepartment(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 min-w-0 sm:min-w-[160px]"
|
||||
>
|
||||
<option value="all">Tüm Departmanlar</option>
|
||||
{departments.map((dept) => (
|
||||
<option key={dept?.id} value={dept?.id}>
|
||||
{dept?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content - List or Card View */}
|
||||
{viewMode === 'list' ? (
|
||||
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<DataTable data={filteredPositions} columns={columns} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredPositions.map((position) => (
|
||||
<PositionCard key={position.id} position={position} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{filteredPositions.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<FaBriefcase className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||
<h3 className="text-base font-medium text-gray-900 mb-2">Pozisyon bulunamadı</h3>
|
||||
<p className="text-sm text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<JobPositionFormModal
|
||||
isOpen={isFormModalOpen}
|
||||
onClose={closeFormModal}
|
||||
onSave={handleSavePosition}
|
||||
position={selectedPosition}
|
||||
title={modalTitle}
|
||||
/>
|
||||
|
||||
<JobPositionViewModal
|
||||
isOpen={isViewModalOpen}
|
||||
onClose={closeViewModal}
|
||||
position={selectedPosition}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobPositions
|
||||
|
|
@ -24,7 +24,6 @@ import {
|
|||
FaDownload,
|
||||
} from 'react-icons/fa'
|
||||
import LoadingSpinner from '../../../components/common/LoadingSpinner'
|
||||
import { EmployeeDto } from '../../../types/hr'
|
||||
import { mockEmployees } from '../../../mocks/mockEmployees'
|
||||
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
|
||||
import {
|
||||
|
|
@ -67,6 +66,7 @@ import {
|
|||
import { Container } from '@/components/shared'
|
||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import { mockCurrencies } from '@/mocks/mockCurrencies'
|
||||
import { EmployeeDto } from '@/proxy/intranet/models'
|
||||
|
||||
// Custom styles for the slider
|
||||
const sliderStyles = `
|
||||
|
|
|
|||
Loading…
Reference in a new issue