3071 lines
130 KiB
TypeScript
3071 lines
130 KiB
TypeScript
|
|
import React, { useState } from "react";
|
|||
|
|
import {
|
|||
|
|
FaUsers,
|
|||
|
|
FaEye,
|
|||
|
|
FaEdit,
|
|||
|
|
FaPlus,
|
|||
|
|
FaChartBar,
|
|||
|
|
FaTimes,
|
|||
|
|
FaSave,
|
|||
|
|
FaPlay,
|
|||
|
|
FaPoll,
|
|||
|
|
} from "react-icons/fa";
|
|||
|
|
import {
|
|||
|
|
HrEvaluation360,
|
|||
|
|
CampaignStatusEnum,
|
|||
|
|
AssessorTypeEnum,
|
|||
|
|
ParticipantStatusEnum,
|
|||
|
|
QuestionTypeEnum,
|
|||
|
|
ResultStatusEnum,
|
|||
|
|
HrEvaluation360Result,
|
|||
|
|
HrGroupScore,
|
|||
|
|
HrAssessorTypeScore,
|
|||
|
|
} from "../../../types/hr";
|
|||
|
|
import DataTable, { Column } from "../../../components/common/DataTable";
|
|||
|
|
import { mockEvaluation360 } from "../../../mocks/mockEvaluation360";
|
|||
|
|
import { mockEvaluation360Results } from "../../../mocks/mockEvaluation360Results";
|
|||
|
|
import { mockEmployees } from "../../../mocks/mockEmployees";
|
|||
|
|
import { mockDepartments } from "../../../mocks/mockDepartments";
|
|||
|
|
import { mockEvaluation360Templates } from "../../../mocks/mockEvaluation360Templates";
|
|||
|
|
import { mockBusinessParties } from "../../../mocks/mockBusinessParties";
|
|||
|
|
import Widget from "../../../components/common/Widget";
|
|||
|
|
import {
|
|||
|
|
getAssessorTypeDescription,
|
|||
|
|
getAssessorTypeText,
|
|||
|
|
getParticipantStatusColor,
|
|||
|
|
getParticipantStatusText,
|
|||
|
|
getCampaignStatusColor,
|
|||
|
|
getCampaignStatusText,
|
|||
|
|
} from "../../../utils/erp";
|
|||
|
|
|
|||
|
|
const Degree360Evaluation: React.FC = () => {
|
|||
|
|
const [activeTab, setActiveTab] = useState<"campaigns" | "results">(
|
|||
|
|
"campaigns"
|
|||
|
|
);
|
|||
|
|
const [selectedStatus, setSelectedStatus] = useState<string>("all");
|
|||
|
|
const [selectedEmployee, setSelectedEmployee] = useState<string>("all");
|
|||
|
|
const [selectedDepartment, setSelectedDepartment] = useState<string>("all");
|
|||
|
|
|
|||
|
|
// Results tab filters
|
|||
|
|
const [selectedResultsStatus, setSelectedResultsStatus] =
|
|||
|
|
useState<string>("all");
|
|||
|
|
const [selectedResultsEmployee, setSelectedResultsEmployee] =
|
|||
|
|
useState<string>("all");
|
|||
|
|
const [selectedResultsDepartment, setSelectedResultsDepartment] =
|
|||
|
|
useState<string>("all");
|
|||
|
|
const [selectedResultsCampaign, setSelectedResultsCampaign] =
|
|||
|
|
useState<string>("all");
|
|||
|
|
|
|||
|
|
// Modal states
|
|||
|
|
const [showCampaignModal, setShowCampaignModal] = useState(false);
|
|||
|
|
const [showViewModal, setShowViewModal] = useState(false);
|
|||
|
|
const [showEvaluationModal, setShowEvaluationModal] = useState(false);
|
|||
|
|
const [showResultDetailModal, setShowResultDetailModal] = useState(false);
|
|||
|
|
const [showResultEditModal, setShowResultEditModal] = useState(false);
|
|||
|
|
const [selectedCampaign, setSelectedCampaign] =
|
|||
|
|
useState<HrEvaluation360 | null>(null);
|
|||
|
|
const [selectedResult, setSelectedResult] = useState<{
|
|||
|
|
resultId: string;
|
|||
|
|
evaluatedEmployeeName: string;
|
|||
|
|
evaluatorName: string;
|
|||
|
|
campaignName: string;
|
|||
|
|
overallScore: number;
|
|||
|
|
scorePercentage: number;
|
|||
|
|
evaluatedEmployeeId: string;
|
|||
|
|
evaluatorId: string;
|
|||
|
|
campaignId: string;
|
|||
|
|
evaluatorType: AssessorTypeEnum;
|
|||
|
|
status: ParticipantStatusEnum;
|
|||
|
|
completedDate?: Date;
|
|||
|
|
} | null>(null);
|
|||
|
|
|
|||
|
|
// Evaluation states
|
|||
|
|
const [evaluationTarget, setEvaluationTarget] = useState<string>("");
|
|||
|
|
const [evaluationEvaluator, setEvaluationEvaluator] = useState<string>("");
|
|||
|
|
const [selectedAssessorType, setSelectedAssessorType] =
|
|||
|
|
useState<AssessorTypeEnum | null>(null);
|
|||
|
|
const [externalEvaluatorName, setExternalEvaluatorName] =
|
|||
|
|
useState<string>("");
|
|||
|
|
const [externalEvaluatorEmail, setExternalEvaluatorEmail] =
|
|||
|
|
useState<string>("");
|
|||
|
|
const [selectedCustomerId, setSelectedCustomerId] = useState<string>("");
|
|||
|
|
const [evaluatorSearchTerm, setEvaluatorSearchTerm] = useState<string>("");
|
|||
|
|
const [currentCampaignForEvaluation, setCurrentCampaignForEvaluation] =
|
|||
|
|
useState<HrEvaluation360 | null>(null);
|
|||
|
|
|
|||
|
|
// Evaluation responses state
|
|||
|
|
const [evaluationResponses, setEvaluationResponses] = useState<
|
|||
|
|
Record<string, string | number>
|
|||
|
|
>({});
|
|||
|
|
|
|||
|
|
// Edit modal states
|
|||
|
|
const [editResponses, setEditResponses] = useState<
|
|||
|
|
Record<string, string | number>
|
|||
|
|
>({});
|
|||
|
|
const [editManagerComments, setEditManagerComments] = useState<string>("");
|
|||
|
|
const [editHrComments, setEditHrComments] = useState<string>("");
|
|||
|
|
const [editResultStatus, setEditResultStatus] = useState<string>("PENDING");
|
|||
|
|
|
|||
|
|
// Form states
|
|||
|
|
const [campaignFormData, setCampaignFormData] = useState({
|
|||
|
|
name: "",
|
|||
|
|
description: "",
|
|||
|
|
templateId: "",
|
|||
|
|
departmentId: "",
|
|||
|
|
targetEmployees: [] as string[],
|
|||
|
|
startDate: "",
|
|||
|
|
endDate: "",
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Event handlers
|
|||
|
|
const handleCloseModal = () => {
|
|||
|
|
setShowCampaignModal(false);
|
|||
|
|
setShowViewModal(false);
|
|||
|
|
setShowEvaluationModal(false);
|
|||
|
|
setShowResultDetailModal(false);
|
|||
|
|
setShowResultEditModal(false);
|
|||
|
|
setSelectedCampaign(null);
|
|||
|
|
setSelectedResult(null);
|
|||
|
|
setEvaluationTarget("");
|
|||
|
|
setEvaluationEvaluator("");
|
|||
|
|
setSelectedAssessorType(null);
|
|||
|
|
setExternalEvaluatorName("");
|
|||
|
|
setExternalEvaluatorEmail("");
|
|||
|
|
setSelectedCustomerId("");
|
|||
|
|
setEvaluatorSearchTerm("");
|
|||
|
|
setCurrentCampaignForEvaluation(null);
|
|||
|
|
setEvaluationResponses({});
|
|||
|
|
setEditResponses({});
|
|||
|
|
setEditManagerComments("");
|
|||
|
|
setEditHrComments("");
|
|||
|
|
setEditResultStatus("PENDING");
|
|||
|
|
setCampaignFormData({
|
|||
|
|
name: "",
|
|||
|
|
description: "",
|
|||
|
|
templateId: "",
|
|||
|
|
departmentId: "",
|
|||
|
|
targetEmployees: [],
|
|||
|
|
startDate: "",
|
|||
|
|
endDate: "",
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Response handling functions
|
|||
|
|
const handleResponseChange = (questionId: string, value: string | number) => {
|
|||
|
|
setEvaluationResponses((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
[questionId]: value,
|
|||
|
|
}));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Edit response handling function
|
|||
|
|
const handleEditResponseChange = (
|
|||
|
|
questionId: string,
|
|||
|
|
value: string | number
|
|||
|
|
) => {
|
|||
|
|
setEditResponses((prev) => ({
|
|||
|
|
...prev,
|
|||
|
|
[questionId]: value,
|
|||
|
|
}));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Evaluation kaydetme fonksiyonu
|
|||
|
|
const handleSaveEvaluation = () => {
|
|||
|
|
if (
|
|||
|
|
!currentCampaignForEvaluation ||
|
|||
|
|
!evaluationTarget ||
|
|||
|
|
!evaluationEvaluator
|
|||
|
|
) {
|
|||
|
|
alert("Tüm alanları doldurun!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const template = mockEvaluation360Templates.find(
|
|||
|
|
(t) => t.id === currentCampaignForEvaluation.templateId
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!template) {
|
|||
|
|
alert("Template bulunamadı!");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Responses oluştur
|
|||
|
|
const responses = Object.entries(evaluationResponses).map(
|
|||
|
|
([questionId, responseValue]) => {
|
|||
|
|
const question = template.questionGroups
|
|||
|
|
.flatMap((g) => g.questions)
|
|||
|
|
.find((q) => q.id === questionId);
|
|||
|
|
|
|||
|
|
let score = 0;
|
|||
|
|
if (question) {
|
|||
|
|
if (question.questionType === QuestionTypeEnum.Rating) {
|
|||
|
|
score = Number(responseValue);
|
|||
|
|
} else if (question.questionType === QuestionTypeEnum.YesNo) {
|
|||
|
|
score =
|
|||
|
|
responseValue === "yes"
|
|||
|
|
? question.maxRating || 5
|
|||
|
|
: question.minRating || 1;
|
|||
|
|
} else if (
|
|||
|
|
question.questionType === QuestionTypeEnum.MultipleChoice
|
|||
|
|
) {
|
|||
|
|
const option = question.options?.find(
|
|||
|
|
(o) => o.value === Number(responseValue)
|
|||
|
|
);
|
|||
|
|
score = option?.value || 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: `response-${Date.now()}-${Math.random()}`,
|
|||
|
|
participantId: `participant-${Date.now()}`,
|
|||
|
|
questionId,
|
|||
|
|
question,
|
|||
|
|
responseValue,
|
|||
|
|
responseText:
|
|||
|
|
typeof responseValue === "string" ? responseValue : undefined,
|
|||
|
|
score,
|
|||
|
|
submittedDate: new Date(),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Puanları hesapla
|
|||
|
|
const totalScore = responses.reduce(
|
|||
|
|
(sum, response) => sum + response.score,
|
|||
|
|
0
|
|||
|
|
);
|
|||
|
|
const maxScore = template.questionGroups
|
|||
|
|
.flatMap((g) => g.questions)
|
|||
|
|
.reduce((sum, q) => sum + (q.maxRating || 5), 0);
|
|||
|
|
|
|||
|
|
const scorePercentage = maxScore > 0 ? (totalScore / maxScore) * 100 : 0;
|
|||
|
|
|
|||
|
|
// Grup skorlarını hesapla
|
|||
|
|
const groupScores: HrGroupScore[] = template.questionGroups.map((group) => {
|
|||
|
|
const groupResponses = responses.filter((r) =>
|
|||
|
|
group.questions.some((q) => q.id === r.questionId)
|
|||
|
|
);
|
|||
|
|
const groupScore = groupResponses.reduce((sum, r) => sum + r.score, 0);
|
|||
|
|
const groupMaxScore = group.questions.reduce(
|
|||
|
|
(sum, q) => sum + (q.maxRating || 5),
|
|||
|
|
0
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
groupId: group.id,
|
|||
|
|
groupName: group.groupName,
|
|||
|
|
score: groupScore,
|
|||
|
|
maxScore: groupMaxScore,
|
|||
|
|
percentage: groupMaxScore > 0 ? (groupScore / groupMaxScore) * 100 : 0,
|
|||
|
|
responseCount: groupResponses.length,
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Assessor type skorlarını hesapla
|
|||
|
|
const assessorTypeScores: HrAssessorTypeScore[] = [
|
|||
|
|
{
|
|||
|
|
assessorType: selectedAssessorType!,
|
|||
|
|
assessorCount: 1,
|
|||
|
|
averageScore: totalScore,
|
|||
|
|
maxScore: maxScore,
|
|||
|
|
percentage: scorePercentage,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// Evaluation360Result oluştur
|
|||
|
|
const evaluationResult: HrEvaluation360Result = {
|
|||
|
|
id: `result-${Date.now()}`,
|
|||
|
|
campaignId: currentCampaignForEvaluation.id,
|
|||
|
|
employeeId: evaluationTarget,
|
|||
|
|
employee: mockEmployees.find((e) => e.id === evaluationTarget),
|
|||
|
|
participants: [
|
|||
|
|
{
|
|||
|
|
id: `participant-${Date.now()}`,
|
|||
|
|
campaignId: currentCampaignForEvaluation.id,
|
|||
|
|
evaluatedEmployeeId: evaluationTarget,
|
|||
|
|
evaluatedEmployee: mockEmployees.find(
|
|||
|
|
(e) => e.id === evaluationTarget
|
|||
|
|
),
|
|||
|
|
evaluatorId: evaluationEvaluator,
|
|||
|
|
evaluator:
|
|||
|
|
selectedAssessorType === AssessorTypeEnum.External ||
|
|||
|
|
selectedAssessorType === AssessorTypeEnum.Customer
|
|||
|
|
? undefined // External ve Customer için Employee bilgisi yok
|
|||
|
|
: mockEmployees.find((e) => e.id === evaluationEvaluator),
|
|||
|
|
evaluatorType: selectedAssessorType!,
|
|||
|
|
status: ParticipantStatusEnum.Completed,
|
|||
|
|
invitedDate: new Date(),
|
|||
|
|
startedDate: new Date(),
|
|||
|
|
completedDate: new Date(),
|
|||
|
|
responses,
|
|||
|
|
overallScore: totalScore,
|
|||
|
|
notes: "",
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
overallScore: totalScore,
|
|||
|
|
maxPossibleScore: maxScore,
|
|||
|
|
scorePercentage,
|
|||
|
|
groupScores,
|
|||
|
|
assessorTypeScores,
|
|||
|
|
strengths: ["Güçlü iletişim", "Takım çalışması"], // Örnek veriler
|
|||
|
|
developmentAreas: ["Zaman yönetimi", "Teknik beceriler"], // Örnek veriler
|
|||
|
|
actionPlan: ["Zaman yönetimi kursu alma", "Teknik eğitimlere katılma"], // Örnek veriler
|
|||
|
|
managerComments: "",
|
|||
|
|
hrComments: "",
|
|||
|
|
status: ResultStatusEnum.Pending,
|
|||
|
|
generatedDate: new Date(),
|
|||
|
|
approvedBy: undefined,
|
|||
|
|
approvedDate: undefined,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Console'a yazdır
|
|||
|
|
console.log("Evaluation360Result:", evaluationResult);
|
|||
|
|
|
|||
|
|
alert("Değerlendirme başarıyla kaydedildi! Sonuç konsola yazdırıldı.");
|
|||
|
|
handleCloseModal();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleNewCampaign = () => {
|
|||
|
|
setSelectedCampaign(null);
|
|||
|
|
setShowCampaignModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleEditCampaign = (campaign: HrEvaluation360 | null) => {
|
|||
|
|
if (campaign) {
|
|||
|
|
setSelectedCampaign(campaign);
|
|||
|
|
setCampaignFormData({
|
|||
|
|
name: campaign.name,
|
|||
|
|
description: campaign.description,
|
|||
|
|
templateId: campaign.templateId,
|
|||
|
|
departmentId: campaign.departmentId || "",
|
|||
|
|
targetEmployees: campaign.targetEmployees,
|
|||
|
|
startDate: campaign.startDate.toISOString().split("T")[0],
|
|||
|
|
endDate: campaign.endDate.toISOString().split("T")[0],
|
|||
|
|
});
|
|||
|
|
setShowCampaignModal(true);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleViewCampaign = (campaign: HrEvaluation360) => {
|
|||
|
|
setSelectedCampaign(campaign);
|
|||
|
|
setShowViewModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSaveCampaign = () => {
|
|||
|
|
console.log("Değerlendirme kaydediliyor:", campaignFormData);
|
|||
|
|
handleCloseModal();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDepartmentChange = (departmentId: string) => {
|
|||
|
|
setSelectedDepartment(departmentId);
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
departmentId,
|
|||
|
|
targetEmployees: [],
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Değerlendirme yapma handler'ı
|
|||
|
|
const handleStartEvaluation = () => {
|
|||
|
|
setCurrentCampaignForEvaluation(null);
|
|||
|
|
setEvaluationTarget("");
|
|||
|
|
setEvaluationEvaluator("");
|
|||
|
|
setShowEvaluationModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Otomatik değerlendirici belirleme fonksiyonu (360° metodolojisi)
|
|||
|
|
const getEvaluatorsByType = (
|
|||
|
|
targetEmployeeId: string,
|
|||
|
|
assessorType: AssessorTypeEnum
|
|||
|
|
) => {
|
|||
|
|
const targetEmployee = mockEmployees.find(
|
|||
|
|
(emp) => emp.id === targetEmployeeId
|
|||
|
|
);
|
|||
|
|
if (!targetEmployee) return [];
|
|||
|
|
|
|||
|
|
switch (assessorType) {
|
|||
|
|
case AssessorTypeEnum.Self: {
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
id: targetEmployee.id,
|
|||
|
|
name: targetEmployee.fullName,
|
|||
|
|
title: targetEmployee.jobPosition?.name,
|
|||
|
|
department:
|
|||
|
|
mockDepartments.find((d) => d.id === targetEmployee.departmantId)
|
|||
|
|
?.name || "",
|
|||
|
|
type: "employee",
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.Manager: {
|
|||
|
|
const managers = mockEmployees.filter(
|
|||
|
|
(emp) =>
|
|||
|
|
emp.departmantId === targetEmployee.departmantId &&
|
|||
|
|
(typeof emp.jobPosition?.level === "number" &&
|
|||
|
|
typeof targetEmployee.jobPosition?.level === "number"
|
|||
|
|
? emp.jobPosition?.level > targetEmployee.jobPosition?.level
|
|||
|
|
: emp.jobPosition?.name.includes("Müdür") ||
|
|||
|
|
emp.jobPosition?.name.includes("Manager"))
|
|||
|
|
);
|
|||
|
|
return managers.map((emp) => ({
|
|||
|
|
id: emp.id,
|
|||
|
|
name: emp.fullName,
|
|||
|
|
title: emp.jobPosition?.name,
|
|||
|
|
department:
|
|||
|
|
mockDepartments.find((d) => d.id === emp.departmantId)?.name || "",
|
|||
|
|
type: "employee",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.Peer: {
|
|||
|
|
const peers = mockEmployees.filter(
|
|||
|
|
(emp) =>
|
|||
|
|
emp.id !== targetEmployeeId &&
|
|||
|
|
emp.departmantId === targetEmployee.departmantId &&
|
|||
|
|
emp.jobPosition?.level === targetEmployee.jobPosition?.level
|
|||
|
|
);
|
|||
|
|
return peers.map((emp) => ({
|
|||
|
|
id: emp.id,
|
|||
|
|
name: emp.fullName,
|
|||
|
|
title: emp.jobPosition?.name,
|
|||
|
|
department:
|
|||
|
|
mockDepartments.find((d) => d.id === emp.departmantId)?.name || "",
|
|||
|
|
type: "employee",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.Subordinate: {
|
|||
|
|
const subordinates = mockEmployees.filter(
|
|||
|
|
(emp) =>
|
|||
|
|
emp.departmantId === targetEmployee.departmantId &&
|
|||
|
|
(typeof emp.jobPosition?.level === "number" &&
|
|||
|
|
typeof targetEmployee.jobPosition?.level === "number"
|
|||
|
|
? emp.jobPosition?.level < targetEmployee.jobPosition?.level
|
|||
|
|
: !emp.jobPosition?.name.includes("Müdür") &&
|
|||
|
|
!emp.jobPosition?.name.includes("Manager"))
|
|||
|
|
);
|
|||
|
|
return subordinates.map((emp) => ({
|
|||
|
|
id: emp.id,
|
|||
|
|
name: emp.fullName,
|
|||
|
|
title: emp.jobPosition?.name,
|
|||
|
|
department:
|
|||
|
|
mockDepartments.find((d) => d.id === emp.departmantId)?.name || "",
|
|||
|
|
type: "employee",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.Customer: {
|
|||
|
|
return mockBusinessParties.map((customer) => ({
|
|||
|
|
id: customer.id,
|
|||
|
|
name: customer.name,
|
|||
|
|
title: "Müşteri Temsilcisi",
|
|||
|
|
department: "Müşteri",
|
|||
|
|
type: "customer",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.External: {
|
|||
|
|
return [
|
|||
|
|
{
|
|||
|
|
id: "external",
|
|||
|
|
name: "Dış Değerlendirici",
|
|||
|
|
title: "Harici",
|
|||
|
|
department: "Dış Paydaş",
|
|||
|
|
type: "external",
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.HRUpperManagement: {
|
|||
|
|
const hrEmployees = mockEmployees.filter(
|
|||
|
|
(emp) =>
|
|||
|
|
emp.id !== targetEmployeeId &&
|
|||
|
|
(emp.departmantId === "hr" || emp.departmantId === "3") // HR departmanı
|
|||
|
|
);
|
|||
|
|
return hrEmployees.map((emp) => ({
|
|||
|
|
id: emp.id,
|
|||
|
|
name: emp.fullName,
|
|||
|
|
title: emp.jobPosition?.name,
|
|||
|
|
department:
|
|||
|
|
mockDepartments.find((d) => d.id === emp.departmantId)?.name || "",
|
|||
|
|
type: "employee",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case AssessorTypeEnum.OtherDepartment: {
|
|||
|
|
const otherDepartmentEmployees = mockEmployees.filter(
|
|||
|
|
(emp) =>
|
|||
|
|
emp.id !== targetEmployeeId &&
|
|||
|
|
emp.departmantId !== targetEmployee.departmantId
|
|||
|
|
);
|
|||
|
|
return otherDepartmentEmployees.map((emp) => ({
|
|||
|
|
id: emp.id,
|
|||
|
|
name: emp.fullName,
|
|||
|
|
title: emp.jobPosition?.name,
|
|||
|
|
department:
|
|||
|
|
mockDepartments.find((d) => d.id === emp.departmantId)?.name || "",
|
|||
|
|
type: "employee",
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Değerlendiren bilgisini getiren fonksiyon
|
|||
|
|
const getEvaluatorInfo = () => {
|
|||
|
|
if (evaluationEvaluator === "external") {
|
|||
|
|
return externalEvaluatorName;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (selectedAssessorType === AssessorTypeEnum.Customer) {
|
|||
|
|
const customer = mockBusinessParties.find(
|
|||
|
|
(c) => c.id === selectedCustomerId
|
|||
|
|
);
|
|||
|
|
return customer ? customer.name : "Müşteri Bulunamadı";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const employee = mockEmployees.find((e) => e.id === evaluationEvaluator);
|
|||
|
|
return employee ? employee.fullName : "Çalışan Bulunamadı";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Template'deki izin verilen assessor tiplerini getir
|
|||
|
|
const getAllowedAssessorTypes = () => {
|
|||
|
|
if (!currentCampaignForEvaluation) return [];
|
|||
|
|
|
|||
|
|
const template = mockEvaluation360Templates.find(
|
|||
|
|
(t) => t.id === currentCampaignForEvaluation.templateId
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return template?.assessorTypes || [];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Helper function to get participants for a campaign from results
|
|||
|
|
const getCampaignParticipants = (campaignId: string) => {
|
|||
|
|
return mockEvaluation360Results
|
|||
|
|
.filter((result) => result.campaignId === campaignId)
|
|||
|
|
.flatMap((result) => result.participants);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Filtered data
|
|||
|
|
const filteredCampaigns = mockEvaluation360.filter((campaign) => {
|
|||
|
|
if (selectedStatus !== "all" && campaign.status !== selectedStatus)
|
|||
|
|
return false;
|
|||
|
|
if (
|
|||
|
|
selectedDepartment !== "all" &&
|
|||
|
|
campaign.departmentId !== selectedDepartment
|
|||
|
|
)
|
|||
|
|
return false;
|
|||
|
|
if (selectedEmployee !== "all") {
|
|||
|
|
const participants = getCampaignParticipants(campaign.id);
|
|||
|
|
const hasEmployee = participants.some(
|
|||
|
|
(p) =>
|
|||
|
|
p.evaluatedEmployeeId === selectedEmployee ||
|
|||
|
|
p.evaluatorId === selectedEmployee
|
|||
|
|
);
|
|||
|
|
if (!hasEmployee) return false;
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const filteredEmployeesByDepartment = mockEmployees.filter(
|
|||
|
|
(emp) =>
|
|||
|
|
selectedDepartment === "all" || emp.departmantId === selectedDepartment
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Statistics
|
|||
|
|
const totalActiveCampaigns = mockEvaluation360.filter(
|
|||
|
|
(c) => c.status === CampaignStatusEnum.Active
|
|||
|
|
).length;
|
|||
|
|
|
|||
|
|
const allParticipants = mockEvaluation360Results.flatMap(
|
|||
|
|
(result) => result.participants
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const totalCompletedEvaluations = allParticipants.filter(
|
|||
|
|
(p) => p.status === ParticipantStatusEnum.Completed
|
|||
|
|
).length;
|
|||
|
|
|
|||
|
|
const totalPendingEvaluations = allParticipants.filter(
|
|||
|
|
(p) =>
|
|||
|
|
p.status === ParticipantStatusEnum.Invited ||
|
|||
|
|
p.status === ParticipantStatusEnum.Started
|
|||
|
|
).length;
|
|||
|
|
|
|||
|
|
const totalParticipants = allParticipants.length;
|
|||
|
|
|
|||
|
|
// Results helper functions
|
|||
|
|
// Participant evaluation helper functions
|
|||
|
|
const handleViewEvaluationDetail = (result: {
|
|||
|
|
resultId: string;
|
|||
|
|
evaluatedEmployeeName: string;
|
|||
|
|
evaluatorName: string;
|
|||
|
|
campaignName: string;
|
|||
|
|
overallScore: number;
|
|||
|
|
scorePercentage: number;
|
|||
|
|
evaluatedEmployeeId: string;
|
|||
|
|
evaluatorId: string;
|
|||
|
|
campaignId: string;
|
|||
|
|
evaluatorType: AssessorTypeEnum;
|
|||
|
|
status: ParticipantStatusEnum;
|
|||
|
|
completedDate?: Date;
|
|||
|
|
}) => {
|
|||
|
|
// Değerlendirme detaylarını görüntüle
|
|||
|
|
console.log("Viewing evaluation detail:", result);
|
|||
|
|
setSelectedResult(result);
|
|||
|
|
setShowResultDetailModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleEditEvaluationDetail = (result: {
|
|||
|
|
resultId: string;
|
|||
|
|
evaluatedEmployeeName: string;
|
|||
|
|
evaluatorName: string;
|
|||
|
|
campaignName: string;
|
|||
|
|
overallScore: number;
|
|||
|
|
scorePercentage: number;
|
|||
|
|
evaluatedEmployeeId: string;
|
|||
|
|
evaluatorId: string;
|
|||
|
|
campaignId: string;
|
|||
|
|
evaluatorType: AssessorTypeEnum;
|
|||
|
|
status: ParticipantStatusEnum;
|
|||
|
|
completedDate?: Date;
|
|||
|
|
}) => {
|
|||
|
|
// Değerlendirme detaylarını düzenle
|
|||
|
|
console.log("Editing evaluation detail:", result);
|
|||
|
|
|
|||
|
|
// Mevcut evaluation result'ını bul
|
|||
|
|
const evaluationResult = mockEvaluation360Results.find(
|
|||
|
|
(r) => r.id === result.resultId
|
|||
|
|
);
|
|||
|
|
if (evaluationResult) {
|
|||
|
|
// Mevcut cevapları yükle
|
|||
|
|
const currentResponses: Record<string, string | number> = {};
|
|||
|
|
evaluationResult.participants.forEach((participant) => {
|
|||
|
|
participant.responses.forEach((response) => {
|
|||
|
|
currentResponses[response.questionId] = response.responseValue;
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
setEditResponses(currentResponses);
|
|||
|
|
setEditManagerComments(evaluationResult.managerComments || "");
|
|||
|
|
setEditHrComments(evaluationResult.hrComments || "");
|
|||
|
|
setEditResultStatus(evaluationResult.status);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setSelectedResult(result);
|
|||
|
|
setShowResultEditModal(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getAssessorTypeLabel = (assessorType: AssessorTypeEnum): string => {
|
|||
|
|
return getAssessorTypeText(assessorType);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getAllEvaluationParticipants = () => {
|
|||
|
|
// Tüm sonuçlardan participant verilerini al
|
|||
|
|
const allParticipants = mockEvaluation360Results.flatMap((result) =>
|
|||
|
|
result.participants.map((participant) => ({
|
|||
|
|
...participant,
|
|||
|
|
resultId: result.id,
|
|||
|
|
evaluatedEmployeeName:
|
|||
|
|
mockEmployees.find((e) => e.id === participant.evaluatedEmployeeId)
|
|||
|
|
?.fullName || "Bilinmiyor",
|
|||
|
|
evaluatorName:
|
|||
|
|
mockEmployees.find((e) => e.id === participant.evaluatorId)
|
|||
|
|
?.fullName || "Bilinmiyor",
|
|||
|
|
campaignName:
|
|||
|
|
mockEvaluation360.find((c) => c.id === participant.campaignId)
|
|||
|
|
?.name || "Bilinmiyor",
|
|||
|
|
overallScore: result.overallScore,
|
|||
|
|
scorePercentage: result.scorePercentage,
|
|||
|
|
result: result,
|
|||
|
|
}))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Filtreleme işlemi
|
|||
|
|
return allParticipants.filter((participant) => {
|
|||
|
|
// Durum filtresi
|
|||
|
|
if (
|
|||
|
|
selectedResultsStatus !== "all" &&
|
|||
|
|
participant.status !== selectedResultsStatus
|
|||
|
|
) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Kampanya filtresi
|
|||
|
|
if (
|
|||
|
|
selectedResultsCampaign !== "all" &&
|
|||
|
|
participant.campaignId !== selectedResultsCampaign
|
|||
|
|
) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Değerlendirilecek kişi filtresi
|
|||
|
|
if (
|
|||
|
|
selectedResultsEmployee !== "all" &&
|
|||
|
|
participant.evaluatedEmployeeId !== selectedResultsEmployee
|
|||
|
|
) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Departman filtresi
|
|||
|
|
if (selectedResultsDepartment !== "all") {
|
|||
|
|
const evaluatedEmployee = mockEmployees.find(
|
|||
|
|
(e) => e.id === participant.evaluatedEmployeeId
|
|||
|
|
);
|
|||
|
|
if (
|
|||
|
|
!evaluatedEmployee ||
|
|||
|
|
evaluatedEmployee.departmantId !== selectedResultsDepartment
|
|||
|
|
) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Columns for campaigns table
|
|||
|
|
const campaignColumns: Column<HrEvaluation360>[] = [
|
|||
|
|
{
|
|||
|
|
key: "name",
|
|||
|
|
header: "Değerlendirme Adı",
|
|||
|
|
sortable: true,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "departmentId",
|
|||
|
|
header: "Departman",
|
|||
|
|
render: (item: HrEvaluation360) => {
|
|||
|
|
const department = mockDepartments.find(
|
|||
|
|
(d) => d.id === item.departmentId
|
|||
|
|
);
|
|||
|
|
return department?.name || "Tüm Departmanlar";
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "templateId",
|
|||
|
|
header: "Şablon",
|
|||
|
|
render: (item: HrEvaluation360) => {
|
|||
|
|
const template = mockEvaluation360Templates.find(
|
|||
|
|
(t) => t.id === item.templateId
|
|||
|
|
);
|
|||
|
|
return template?.name || "Bilinmiyor";
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "status",
|
|||
|
|
header: "Durum",
|
|||
|
|
render: (item: HrEvaluation360) => (
|
|||
|
|
<span
|
|||
|
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getCampaignStatusColor(
|
|||
|
|
item.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getCampaignStatusText(item.status)}
|
|||
|
|
</span>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "startDate",
|
|||
|
|
header: "Başlangıç",
|
|||
|
|
render: (item: HrEvaluation360) =>
|
|||
|
|
item.startDate.toLocaleDateString("tr-TR"),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "endDate",
|
|||
|
|
header: "Bitiş",
|
|||
|
|
render: (item: HrEvaluation360) =>
|
|||
|
|
item.endDate.toLocaleDateString("tr-TR"),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "participants",
|
|||
|
|
header: "Katılımcı",
|
|||
|
|
render: (item: HrEvaluation360) => {
|
|||
|
|
const participants = getCampaignParticipants(item.id);
|
|||
|
|
return `${participants.length} kişi`;
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "id",
|
|||
|
|
header: "İşlemler",
|
|||
|
|
render: (item: HrEvaluation360) => (
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleViewCampaign(item)}
|
|||
|
|
className="p-1 text-blue-600 hover:text-blue-800"
|
|||
|
|
title="Görüntüle"
|
|||
|
|
>
|
|||
|
|
<FaEye className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => handleEditCampaign(item)}
|
|||
|
|
className="p-1 text-green-600 hover:text-green-800"
|
|||
|
|
title="Düzenle"
|
|||
|
|
>
|
|||
|
|
<FaEdit className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mt-2 space-y-3">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<div>
|
|||
|
|
<h1 className="text-xl font-bold text-gray-900">
|
|||
|
|
360° Değerlendirme Sistemi
|
|||
|
|
</h1>
|
|||
|
|
<p className="text-gray-600 mt-1">
|
|||
|
|
Çok yönlü performans değerlendirme sistemi
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Statistics Cards */}
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
|||
|
|
<Widget
|
|||
|
|
title="Aktif Değerlendirmeler"
|
|||
|
|
value={totalActiveCampaigns}
|
|||
|
|
color="blue"
|
|||
|
|
icon="FaPlay"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Widget
|
|||
|
|
title="Tamamlanan Değerlendirmeler"
|
|||
|
|
value={totalCompletedEvaluations}
|
|||
|
|
color="green"
|
|||
|
|
icon="FaCheckCircle"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Widget
|
|||
|
|
title="Bekleyen Değerlendirmeler"
|
|||
|
|
value={totalPendingEvaluations}
|
|||
|
|
color="yellow"
|
|||
|
|
icon="FaPoll"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Widget
|
|||
|
|
title="Katılımcılar"
|
|||
|
|
value={totalParticipants}
|
|||
|
|
color="purple"
|
|||
|
|
icon="FaUsers"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Tabs */}
|
|||
|
|
<div className="bg-white rounded-lg shadow-sm border">
|
|||
|
|
<div className="border-b border-gray-200">
|
|||
|
|
<nav className="-mb-px flex space-x-2 px-2">
|
|||
|
|
{[
|
|||
|
|
{
|
|||
|
|
key: "campaigns",
|
|||
|
|
label: "Değerlendirme Kampanyaları",
|
|||
|
|
icon: FaUsers,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "results",
|
|||
|
|
label: "Değerlendirme Sonuçları",
|
|||
|
|
icon: FaChartBar,
|
|||
|
|
},
|
|||
|
|
].map(({ key, label, icon: Icon }) => (
|
|||
|
|
<button
|
|||
|
|
key={key}
|
|||
|
|
onClick={() => setActiveTab(key as "campaigns" | "results")}
|
|||
|
|
className={`flex items-center gap-2 py-1.5 px-2.5 border-b-2 font-medium text-sm ${
|
|||
|
|
activeTab === key
|
|||
|
|
? "border-blue-500 text-blue-600"
|
|||
|
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<Icon className="w-4 h-4" />
|
|||
|
|
{label}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</nav>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="p-3">
|
|||
|
|
{activeTab === "campaigns" && (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{/* Filters and Actions */}
|
|||
|
|
<div className="flex flex-col sm:flex-row gap-3 justify-between">
|
|||
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|||
|
|
<select
|
|||
|
|
value={selectedStatus}
|
|||
|
|
onChange={(e) => setSelectedStatus(e.target.value)}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Durumlar</option>
|
|||
|
|
<option value={CampaignStatusEnum.Draft}>Taslak</option>
|
|||
|
|
<option value={CampaignStatusEnum.Active}>Aktif</option>
|
|||
|
|
<option value={CampaignStatusEnum.Completed}>
|
|||
|
|
Tamamlandı
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedDepartment}
|
|||
|
|
onChange={(e) => setSelectedDepartment(e.target.value)}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Departmanlar</option>
|
|||
|
|
{mockDepartments.map((department) => (
|
|||
|
|
<option key={department.id} value={department.id}>
|
|||
|
|
{department.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedEmployee}
|
|||
|
|
onChange={(e) => setSelectedEmployee(e.target.value)}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Personel</option>
|
|||
|
|
{mockEmployees.map((employee) => (
|
|||
|
|
<option key={employee.id} value={employee.id}>
|
|||
|
|
{employee.firstName} {employee.lastName}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
onClick={handleNewCampaign}
|
|||
|
|
className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
|
|||
|
|
>
|
|||
|
|
<FaPlus className="w-4 h-4" />
|
|||
|
|
Yeni Değerlendirme
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleStartEvaluation}
|
|||
|
|
className="flex items-center gap-2 px-3 py-1.5 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
|||
|
|
>
|
|||
|
|
<FaPoll className="w-4 h-4" />
|
|||
|
|
Değerlendirme Yap
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Campaigns Table */}
|
|||
|
|
<DataTable data={filteredCampaigns} columns={campaignColumns} />
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{activeTab === "results" && (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{/* Results Filters */}
|
|||
|
|
<div className="flex flex-col sm:flex-row gap-3 justify-between">
|
|||
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|||
|
|
<select
|
|||
|
|
value={selectedResultsCampaign}
|
|||
|
|
onChange={(e) => setSelectedResultsCampaign(e.target.value)}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Değerlendirmeler</option>
|
|||
|
|
{mockEvaluation360.map((campaign) => (
|
|||
|
|
<option key={campaign.id} value={campaign.id}>
|
|||
|
|
{campaign.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedResultsStatus}
|
|||
|
|
onChange={(e) => setSelectedResultsStatus(e.target.value)}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Durumlar</option>
|
|||
|
|
<option value={ParticipantStatusEnum.Invited}>
|
|||
|
|
Davet Edildi
|
|||
|
|
</option>
|
|||
|
|
<option value={ParticipantStatusEnum.Started}>
|
|||
|
|
Başladı
|
|||
|
|
</option>
|
|||
|
|
<option value={ParticipantStatusEnum.Completed}>
|
|||
|
|
Tamamlandı
|
|||
|
|
</option>
|
|||
|
|
<option value={ParticipantStatusEnum.Expired}>
|
|||
|
|
Süresi Dolmuş
|
|||
|
|
</option>
|
|||
|
|
<option value={ParticipantStatusEnum.Declined}>
|
|||
|
|
Reddedildi
|
|||
|
|
</option>
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedResultsDepartment}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setSelectedResultsDepartment(e.target.value)
|
|||
|
|
}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Departmanlar</option>
|
|||
|
|
{mockDepartments.map((department) => (
|
|||
|
|
<option key={department.id} value={department.id}>
|
|||
|
|
{department.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<select
|
|||
|
|
value={selectedResultsEmployee}
|
|||
|
|
onChange={(e) => setSelectedResultsEmployee(e.target.value)}
|
|||
|
|
className="border border-gray-300 rounded-md px-2.5 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="all">Tüm Personel</option>
|
|||
|
|
{mockEmployees.map((employee) => (
|
|||
|
|
<option key={employee.id} value={employee.id}>
|
|||
|
|
{employee.firstName} {employee.lastName}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Results Summary */}
|
|||
|
|
|
|||
|
|
<div className="bg-white rounded-lg border overflow-hidden">
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|||
|
|
<thead className="bg-gray-50">
|
|||
|
|
<tr>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Değerlendirme Adı
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Değerlendirilecek Kişi
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Değerlendiren
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Toplam İlerleme
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Değerlendirme Durumu
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Toplam Puan
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Puan Yüzdesi
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
İşlemler
|
|||
|
|
</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|||
|
|
{getAllEvaluationParticipants().map((result) => (
|
|||
|
|
<tr
|
|||
|
|
key={`${result.resultId}-${result.evaluatedEmployeeId}-${result.evaluatorId}`}
|
|||
|
|
className="hover:bg-gray-50 text-xs"
|
|||
|
|
>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-gray-500">
|
|||
|
|
<div className="flex flex-col">
|
|||
|
|
<span className="font-medium text-gray-900">
|
|||
|
|
{result.campaignName}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xs text-gray-500">
|
|||
|
|
{result.completedDate
|
|||
|
|
? new Date(
|
|||
|
|
result.completedDate
|
|||
|
|
).toLocaleDateString("tr-TR")
|
|||
|
|
: "-"}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-gray-500">
|
|||
|
|
{result.evaluatedEmployeeName}
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-gray-500">
|
|||
|
|
<div className="flex flex-col">
|
|||
|
|
<span className="font-medium text-gray-900">
|
|||
|
|
{result.evaluatorName}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xs text-gray-500">
|
|||
|
|
{getAssessorTypeLabel(result.evaluatorType)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm">
|
|||
|
|
<div className="flex items-center">
|
|||
|
|
<div className="w-16 bg-gray-200 rounded-full h-2 mr-2">
|
|||
|
|
<div
|
|||
|
|
className="bg-blue-600 h-2 rounded-full"
|
|||
|
|
style={{
|
|||
|
|
width: `${
|
|||
|
|
result.status ===
|
|||
|
|
ParticipantStatusEnum.Completed
|
|||
|
|
? 100
|
|||
|
|
: 50
|
|||
|
|
}%`,
|
|||
|
|
}}
|
|||
|
|
></div>
|
|||
|
|
</div>
|
|||
|
|
<span className="text-sm text-gray-600">
|
|||
|
|
{result.status ===
|
|||
|
|
ParticipantStatusEnum.Completed
|
|||
|
|
? 100
|
|||
|
|
: 50}
|
|||
|
|
%
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
|||
|
|
<span
|
|||
|
|
className={`inline-block px-2 py-1 text-xs rounded-full ${getParticipantStatusColor(
|
|||
|
|
result.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getParticipantStatusText(result.status)}
|
|||
|
|
</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
|||
|
|
{result.overallScore}/100
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
|||
|
|
%{result.scorePercentage}
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm font-medium">
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button
|
|||
|
|
onClick={() =>
|
|||
|
|
handleViewEvaluationDetail(result)
|
|||
|
|
}
|
|||
|
|
className="text-blue-600 hover:text-blue-900"
|
|||
|
|
title="Değerlendirme Detayını Görüntüle"
|
|||
|
|
>
|
|||
|
|
<FaEye className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() =>
|
|||
|
|
handleEditEvaluationDetail(result)
|
|||
|
|
}
|
|||
|
|
className="text-green-600 hover:text-green-900"
|
|||
|
|
title="Değerlendirme Detayını Düzenle"
|
|||
|
|
>
|
|||
|
|
<FaEdit className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
))}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Campaign Modal */}
|
|||
|
|
{showCampaignModal && (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-2">
|
|||
|
|
<div className="bg-white rounded-lg w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
|||
|
|
<div className="flex justify-between items-center p-2.5 border-b">
|
|||
|
|
<h2 className="text-base font-semibold">
|
|||
|
|
{selectedCampaign
|
|||
|
|
? "Değerlendirme Düzenle"
|
|||
|
|
: "Yeni Değerlendirme"}
|
|||
|
|
</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="p-1.5 text-gray-400 hover:text-gray-600"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-3 space-y-2.5">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Değerlendirme Adı
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={campaignFormData.name}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
name: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2 py-1 text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Açıklama
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
value={campaignFormData.description}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
description: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
rows={1}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2 py-1 text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Değerlendirme Şablonu
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={campaignFormData.templateId}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
templateId: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">Şablon Seçin</option>
|
|||
|
|
{mockEvaluation360Templates.map((template) => (
|
|||
|
|
<option key={template.id} value={template.id}>
|
|||
|
|
{template.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Departman
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
value={campaignFormData.departmentId}
|
|||
|
|
onChange={(e) => handleDepartmentChange(e.target.value)}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2 py-1 text-sm"
|
|||
|
|
>
|
|||
|
|
<option value="">Departman Seçin</option>
|
|||
|
|
{mockDepartments.map((department) => (
|
|||
|
|
<option key={department.id} value={department.id}>
|
|||
|
|
{department.name}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{campaignFormData.departmentId && (
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Hedef Personel
|
|||
|
|
</label>
|
|||
|
|
<div className="border border-gray-300 rounded-md p-1.5 max-h-28 overflow-y-auto">
|
|||
|
|
{filteredEmployeesByDepartment.map((employee) => (
|
|||
|
|
<label
|
|||
|
|
key={employee.id}
|
|||
|
|
className="flex items-center mb-2"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
type="checkbox"
|
|||
|
|
checked={campaignFormData.targetEmployees.includes(
|
|||
|
|
employee.id
|
|||
|
|
)}
|
|||
|
|
onChange={(e) => {
|
|||
|
|
if (e.target.checked) {
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
targetEmployees: [
|
|||
|
|
...campaignFormData.targetEmployees,
|
|||
|
|
employee.id,
|
|||
|
|
],
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
targetEmployees:
|
|||
|
|
campaignFormData.targetEmployees.filter(
|
|||
|
|
(id) => id !== employee.id
|
|||
|
|
),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
{employee.firstName} {employee.lastName} -{" "}
|
|||
|
|
{employee.jobPosition?.name || "Pozisyon Belirtilmemiş"}
|
|||
|
|
</label>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="grid grid-cols-2 gap-2.5">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Başlangıç Tarihi
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={campaignFormData.startDate}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
startDate: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2 py-1 text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Bitiş Tarihi
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="date"
|
|||
|
|
value={campaignFormData.endDate}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setCampaignFormData({
|
|||
|
|
...campaignFormData,
|
|||
|
|
endDate: e.target.value,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2 py-1 text-sm"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-end gap-2 p-2.5 border-t">
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="px-2.5 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
|
|||
|
|
>
|
|||
|
|
İptal
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleSaveCampaign}
|
|||
|
|
className="flex items-center gap-2 px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|||
|
|
>
|
|||
|
|
<FaSave className="w-4 h-4" />
|
|||
|
|
{selectedCampaign ? "Güncelle" : "Kaydet"}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* View Modal */}
|
|||
|
|
{showViewModal && selectedCampaign && (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-2">
|
|||
|
|
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|||
|
|
<div className="flex justify-between items-center p-2.5 border-b">
|
|||
|
|
<h2 className="text-base font-semibold">
|
|||
|
|
{selectedCampaign.name}
|
|||
|
|
</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="p-1.5 text-gray-400 hover:text-gray-600"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-3 space-y-3">
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
Değerlendirme Bilgileri
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm font-medium text-gray-600">
|
|||
|
|
Adı:
|
|||
|
|
</span>
|
|||
|
|
<p className="text-sm text-gray-900">
|
|||
|
|
{selectedCampaign.name}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm font-medium text-gray-600">
|
|||
|
|
Açıklama:
|
|||
|
|
</span>
|
|||
|
|
<p className="text-sm text-gray-900">
|
|||
|
|
{selectedCampaign.description}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm font-medium text-gray-600">
|
|||
|
|
Durum:
|
|||
|
|
</span>
|
|||
|
|
<span
|
|||
|
|
className={`ml-2 px-3 py-1 text-xs font-medium rounded-full ${getCampaignStatusColor(
|
|||
|
|
selectedCampaign.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getCampaignStatusText(selectedCampaign.status)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm font-medium text-gray-600">
|
|||
|
|
Tarih Aralığı:
|
|||
|
|
</span>
|
|||
|
|
<p className="text-sm text-gray-900">
|
|||
|
|
{selectedCampaign.startDate.toLocaleDateString("tr-TR")}{" "}
|
|||
|
|
- {selectedCampaign.endDate.toLocaleDateString("tr-TR")}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
Katılımcı İstatistikleri
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{(() => {
|
|||
|
|
const participants = getCampaignParticipants(
|
|||
|
|
selectedCampaign.id
|
|||
|
|
);
|
|||
|
|
const completed = participants.filter(
|
|||
|
|
(p) => p.status === ParticipantStatusEnum.Completed
|
|||
|
|
);
|
|||
|
|
const started = participants.filter(
|
|||
|
|
(p) => p.status === ParticipantStatusEnum.Started
|
|||
|
|
);
|
|||
|
|
const invited = participants.filter(
|
|||
|
|
(p) => p.status === ParticipantStatusEnum.Invited
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-gray-600">
|
|||
|
|
Toplam:
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm font-medium text-gray-900">
|
|||
|
|
{participants.length}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-gray-600">
|
|||
|
|
Tamamlanan:
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm font-medium text-green-600">
|
|||
|
|
{completed.length}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-gray-600">
|
|||
|
|
Devam Eden:
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm font-medium text-blue-600">
|
|||
|
|
{started.length}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between">
|
|||
|
|
<span className="text-sm text-gray-600">
|
|||
|
|
Bekleyen:
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm font-medium text-yellow-600">
|
|||
|
|
{invited.length}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
Katılımcı Detayları
|
|||
|
|
</h3>
|
|||
|
|
<div className="overflow-x-auto">
|
|||
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|||
|
|
<thead className="bg-gray-50">
|
|||
|
|
<tr>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Hedef Çalışan
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Değerlendiren
|
|||
|
|
</th>
|
|||
|
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Tip
|
|||
|
|
</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-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|||
|
|
Tarih
|
|||
|
|
</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|||
|
|
{getCampaignParticipants(selectedCampaign.id).map(
|
|||
|
|
(participant) => {
|
|||
|
|
const targetEmployee = mockEmployees.find(
|
|||
|
|
(emp) => emp.id === participant.evaluatedEmployeeId
|
|||
|
|
);
|
|||
|
|
const assessorEmployee = mockEmployees.find(
|
|||
|
|
(emp) => emp.id === participant.evaluatorId
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<tr key={participant.id}>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
|||
|
|
{targetEmployee
|
|||
|
|
? `${targetEmployee.firstName} ${targetEmployee.lastName}`
|
|||
|
|
: "Bilinmiyor"}
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-900">
|
|||
|
|
{assessorEmployee
|
|||
|
|
? `${assessorEmployee.firstName} ${assessorEmployee.lastName}`
|
|||
|
|
: "Bilinmiyor"}
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
|||
|
|
{getAssessorTypeText(participant.evaluatorType)}
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap">
|
|||
|
|
<span
|
|||
|
|
className={`px-2 py-1 text-xs font-medium rounded-full ${getParticipantStatusColor(
|
|||
|
|
participant.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getParticipantStatusText(participant.status)}
|
|||
|
|
</span>
|
|||
|
|
</td>
|
|||
|
|
<td className="px-3 py-2 whitespace-nowrap text-sm text-gray-500">
|
|||
|
|
{participant.completedDate
|
|||
|
|
? participant.completedDate.toLocaleDateString(
|
|||
|
|
"tr-TR"
|
|||
|
|
)
|
|||
|
|
: participant.startedDate
|
|||
|
|
? `Başladı: ${participant.startedDate.toLocaleDateString(
|
|||
|
|
"tr-TR"
|
|||
|
|
)}`
|
|||
|
|
: `Davet: ${participant.invitedDate.toLocaleDateString(
|
|||
|
|
"tr-TR"
|
|||
|
|
)}`}
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
)}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-end gap-2 p-2.5 border-t">
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="px-2.5 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
|
|||
|
|
>
|
|||
|
|
Kapat
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
handleCloseModal();
|
|||
|
|
handleEditCampaign(selectedCampaign);
|
|||
|
|
}}
|
|||
|
|
className="flex items-center gap-2 px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|||
|
|
>
|
|||
|
|
<FaEdit className="w-4 h-4" />
|
|||
|
|
Düzenle
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Evaluation Modal */}
|
|||
|
|
{showEvaluationModal && (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-2">
|
|||
|
|
<div className="bg-white rounded-lg w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
|||
|
|
<div className="flex justify-between items-center p-2.5 border-b">
|
|||
|
|
<h2 className="text-base font-semibold">
|
|||
|
|
{!currentCampaignForEvaluation
|
|||
|
|
? "Değerlendirme Yap"
|
|||
|
|
: currentCampaignForEvaluation.name}
|
|||
|
|
</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="p-1.5 text-gray-400 hover:text-gray-600"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-3 space-y-2.5">
|
|||
|
|
{!currentCampaignForEvaluation ? (
|
|||
|
|
/* Adım 0: Değerlendirme seçimi */
|
|||
|
|
<div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
Değerlendirme Seçin
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{mockEvaluation360
|
|||
|
|
.filter(
|
|||
|
|
(campaign) =>
|
|||
|
|
campaign.status === CampaignStatusEnum.Active
|
|||
|
|
)
|
|||
|
|
.map((campaign) => (
|
|||
|
|
<div
|
|||
|
|
key={campaign.id}
|
|||
|
|
className="border rounded-lg p-2.5 hover:bg-gray-50 cursor-pointer"
|
|||
|
|
onClick={() =>
|
|||
|
|
setCurrentCampaignForEvaluation(campaign)
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-900">
|
|||
|
|
{campaign.name}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{campaign.description}
|
|||
|
|
</p>
|
|||
|
|
<div className="flex items-center gap-2 mt-2">
|
|||
|
|
<span
|
|||
|
|
className={`inline-block px-2 py-1 text-xs rounded-full ${getCampaignStatusColor(
|
|||
|
|
campaign.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getCampaignStatusText(campaign.status)}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xs text-gray-500">
|
|||
|
|
{campaign.startDate.toLocaleDateString()} -{" "}
|
|||
|
|
{campaign.endDate.toLocaleDateString()}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<FaPlay className="w-4 h-4 text-gray-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
{mockEvaluation360.filter(
|
|||
|
|
(campaign) => campaign.status === CampaignStatusEnum.Active
|
|||
|
|
).length === 0 && (
|
|||
|
|
<div className="text-center py-8 text-gray-500">
|
|||
|
|
<FaPoll className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|||
|
|
<p>Aktif değerlendirme bulunmuyor</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
) : !evaluationTarget ? (
|
|||
|
|
/* Adım 1: Değerlendirilecek kişi seçimi */
|
|||
|
|
<div>
|
|||
|
|
<div className="mb-4">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setCurrentCampaignForEvaluation(null)}
|
|||
|
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
|||
|
|
>
|
|||
|
|
← Değerlendirme Seçimine Dön
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
1. Adım: Değerlendirilecek Kişiyi Seçin
|
|||
|
|
</h3>
|
|||
|
|
<div className="bg-blue-50 p-2.5 rounded-lg mb-3">
|
|||
|
|
<p className="text-sm text-blue-800">
|
|||
|
|
<strong>Seçili Değerlendirme:</strong>{" "}
|
|||
|
|
{currentCampaignForEvaluation.name}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{mockEmployees
|
|||
|
|
.filter((emp) =>
|
|||
|
|
currentCampaignForEvaluation.targetEmployees.includes(
|
|||
|
|
emp.id
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
.map((employee) => (
|
|||
|
|
<div
|
|||
|
|
key={employee.id}
|
|||
|
|
className="border rounded-lg p-2.5 hover:bg-gray-50 cursor-pointer"
|
|||
|
|
onClick={() => setEvaluationTarget(employee.id)}
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-900">
|
|||
|
|
{employee.fullName}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{employee.jobPosition?.name}
|
|||
|
|
</p>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{
|
|||
|
|
mockDepartments.find(
|
|||
|
|
(d) => d.id === employee.departmantId
|
|||
|
|
)?.name
|
|||
|
|
}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
{mockEmployees.filter((emp) =>
|
|||
|
|
currentCampaignForEvaluation.targetEmployees.includes(
|
|||
|
|
emp.id
|
|||
|
|
)
|
|||
|
|
).length === 0 && (
|
|||
|
|
<div className="text-center py-8 text-gray-500">
|
|||
|
|
<FaUsers className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|||
|
|
<p>Bu değerlendirmede hedef çalışan bulunmuyor</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
) : !selectedAssessorType ? (
|
|||
|
|
/* Adım 2a: Değerlendirici Tipi Seçimi */
|
|||
|
|
<div>
|
|||
|
|
<div className="mb-4">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setEvaluationTarget("")}
|
|||
|
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
|||
|
|
>
|
|||
|
|
← Geri Dön
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
2. Adım: Değerlendirici Tipi Seçin
|
|||
|
|
</h3>
|
|||
|
|
<div className="bg-blue-50 p-2.5 rounded-lg mb-3">
|
|||
|
|
<p className="text-sm text-blue-800">
|
|||
|
|
<strong>Değerlendirilecek:</strong>{" "}
|
|||
|
|
{
|
|||
|
|
mockEmployees.find((e) => e.id === evaluationTarget)
|
|||
|
|
?.fullName
|
|||
|
|
}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Değerlendirici tipi seçimi */}
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{getAllowedAssessorTypes().map((assessorType) => (
|
|||
|
|
<div
|
|||
|
|
key={assessorType}
|
|||
|
|
className="border rounded-lg p-2.5 hover:bg-gray-50 cursor-pointer"
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedAssessorType(assessorType);
|
|||
|
|
setEvaluatorSearchTerm("");
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-900">
|
|||
|
|
{getAssessorTypeText(assessorType)}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{getAssessorTypeDescription(assessorType)}
|
|||
|
|
</p>
|
|||
|
|
{getEvaluatorsByType(evaluationTarget, assessorType)
|
|||
|
|
.length > 0 && (
|
|||
|
|
<span className="inline-block mt-1 px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">
|
|||
|
|
{
|
|||
|
|
getEvaluatorsByType(
|
|||
|
|
evaluationTarget,
|
|||
|
|
assessorType
|
|||
|
|
).length
|
|||
|
|
}{" "}
|
|||
|
|
seçenek mevcut
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : !evaluationEvaluator ? (
|
|||
|
|
/* Adım 2b: Belirli tip değerlendirici seçimi */
|
|||
|
|
<div>
|
|||
|
|
<div className="mb-4">
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedAssessorType(null);
|
|||
|
|
setEvaluationEvaluator("");
|
|||
|
|
setSelectedCustomerId("");
|
|||
|
|
setExternalEvaluatorName("");
|
|||
|
|
setExternalEvaluatorEmail("");
|
|||
|
|
setEvaluatorSearchTerm("");
|
|||
|
|
}}
|
|||
|
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
|||
|
|
>
|
|||
|
|
← Geri Dön
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
2. Adım: {getAssessorTypeText(selectedAssessorType)} Seçin
|
|||
|
|
</h3>
|
|||
|
|
<div className="bg-blue-50 p-2.5 rounded-lg mb-3">
|
|||
|
|
<p className="text-sm text-blue-800">
|
|||
|
|
<strong>Değerlendirilecek:</strong>{" "}
|
|||
|
|
{
|
|||
|
|
mockEmployees.find((e) => e.id === evaluationTarget)
|
|||
|
|
?.fullName
|
|||
|
|
}
|
|||
|
|
<br />
|
|||
|
|
<strong>Değerlendirici Tipi:</strong>{" "}
|
|||
|
|
{getAssessorTypeText(selectedAssessorType)}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{selectedAssessorType === AssessorTypeEnum.External ? (
|
|||
|
|
/* Dış değerlendirici için form */
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Değerlendirici Adı *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={externalEvaluatorName}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setExternalEvaluatorName(e.target.value)
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
placeholder="Değerlendirici adını girin"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
E-posta Adresi *
|
|||
|
|
</label>
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
value={externalEvaluatorEmail}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setExternalEvaluatorEmail(e.target.value)
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
placeholder="E-posta adresini girin"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
{externalEvaluatorName && externalEvaluatorEmail && (
|
|||
|
|
<button
|
|||
|
|
onClick={() => setEvaluationEvaluator("external")}
|
|||
|
|
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 text-sm"
|
|||
|
|
>
|
|||
|
|
Dış Değerlendiriciyi Seç
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
) : selectedAssessorType === AssessorTypeEnum.Customer ? (
|
|||
|
|
/* Müşteri seçimi */
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{/* Arama kutusu */}
|
|||
|
|
<div>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={evaluatorSearchTerm}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setEvaluatorSearchTerm(e.target.value)
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
placeholder="Müşteri adında ara..."
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
{getEvaluatorsByType(
|
|||
|
|
evaluationTarget,
|
|||
|
|
selectedAssessorType
|
|||
|
|
)
|
|||
|
|
.filter((evaluator) =>
|
|||
|
|
evaluator.name
|
|||
|
|
.toLowerCase()
|
|||
|
|
.includes(evaluatorSearchTerm.toLowerCase())
|
|||
|
|
)
|
|||
|
|
.map((evaluator) => (
|
|||
|
|
<div
|
|||
|
|
key={evaluator.id}
|
|||
|
|
className="border rounded-lg p-2.5 hover:bg-gray-50 cursor-pointer"
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedCustomerId(evaluator.id);
|
|||
|
|
setEvaluationEvaluator(evaluator.id);
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-900">
|
|||
|
|
{evaluator.name}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{evaluator.title}
|
|||
|
|
</p>
|
|||
|
|
<span className="inline-block mt-1 px-2 py-1 text-xs rounded-full bg-purple-100 text-purple-800">
|
|||
|
|
{evaluator.department}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
/* Diğer tipler için çalışan seçimi */
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{/* Arama kutusu */}
|
|||
|
|
<div>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={evaluatorSearchTerm}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
setEvaluatorSearchTerm(e.target.value)
|
|||
|
|
}
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
placeholder="Çalışan adında ara..."
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
{getEvaluatorsByType(
|
|||
|
|
evaluationTarget,
|
|||
|
|
selectedAssessorType
|
|||
|
|
)
|
|||
|
|
.filter((evaluator) =>
|
|||
|
|
evaluator.name
|
|||
|
|
.toLowerCase()
|
|||
|
|
.includes(evaluatorSearchTerm.toLowerCase())
|
|||
|
|
)
|
|||
|
|
.map((evaluator) => (
|
|||
|
|
<div
|
|||
|
|
key={evaluator.id}
|
|||
|
|
className="border rounded-lg p-2.5 hover:bg-gray-50 cursor-pointer"
|
|||
|
|
onClick={() => setEvaluationEvaluator(evaluator.id)}
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between items-center">
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-900">
|
|||
|
|
{evaluator.name}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{evaluator.title}
|
|||
|
|
</p>
|
|||
|
|
<span className="inline-block mt-1 px-2 py-1 text-xs rounded-full bg-purple-100 text-purple-800">
|
|||
|
|
{evaluator.department}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<FaUsers className="w-4 h-4 text-gray-400" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
{getEvaluatorsByType(
|
|||
|
|
evaluationTarget,
|
|||
|
|
selectedAssessorType
|
|||
|
|
).length === 0 && (
|
|||
|
|
<div className="text-center py-8 text-gray-500">
|
|||
|
|
<FaUsers className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|||
|
|
<p>Bu kategoride değerlendirici bulunmuyor</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
/* Adım 3: Değerlendirme formu */
|
|||
|
|
<div>
|
|||
|
|
<div className="mb-4">
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
setSelectedAssessorType(null);
|
|||
|
|
setEvaluationEvaluator("");
|
|||
|
|
setSelectedCustomerId("");
|
|||
|
|
setExternalEvaluatorName("");
|
|||
|
|
setExternalEvaluatorEmail("");
|
|||
|
|
setEvaluatorSearchTerm("");
|
|||
|
|
}}
|
|||
|
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
|||
|
|
>
|
|||
|
|
← Geri Dön
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900 mb-2">
|
|||
|
|
3. Adım: Değerlendirme Formu
|
|||
|
|
</h3>
|
|||
|
|
<div className="bg-green-50 p-2.5 rounded-lg mb-3">
|
|||
|
|
<p className="text-sm text-green-800">
|
|||
|
|
<strong>Değerlendirilen:</strong>{" "}
|
|||
|
|
{
|
|||
|
|
mockEmployees.find((e) => e.id === evaluationTarget)
|
|||
|
|
?.fullName
|
|||
|
|
}
|
|||
|
|
<br />
|
|||
|
|
<strong>Değerlendiren:</strong> {getEvaluatorInfo()}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Burada şablonun sorularını göstereceğiz */}
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{currentCampaignForEvaluation &&
|
|||
|
|
mockEvaluation360Templates
|
|||
|
|
.find(
|
|||
|
|
(t) =>
|
|||
|
|
t.id === currentCampaignForEvaluation.templateId
|
|||
|
|
)
|
|||
|
|
?.questionGroups.map((group) => (
|
|||
|
|
<div
|
|||
|
|
key={group.id}
|
|||
|
|
className="border rounded-lg p-2.5"
|
|||
|
|
>
|
|||
|
|
<h4 className="font-medium text-gray-900 mb-2">
|
|||
|
|
{group.groupName}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600 mb-3">
|
|||
|
|
{group.description}
|
|||
|
|
</p>
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{group.questions.map((question, qIndex) => (
|
|||
|
|
<div
|
|||
|
|
key={question.id}
|
|||
|
|
className="bg-gray-50 p-2.5 rounded"
|
|||
|
|
>
|
|||
|
|
<p className="font-medium text-gray-800 mb-3">
|
|||
|
|
{qIndex + 1}. {question.questionText}
|
|||
|
|
{question.isRequired && (
|
|||
|
|
<span className="text-red-500 ml-1">
|
|||
|
|
*
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{/* Soru tipine göre input alanları */}
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.Rating && (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex justify-around items-center">
|
|||
|
|
{Array.from(
|
|||
|
|
{
|
|||
|
|
length:
|
|||
|
|
(question.maxRating || 5) -
|
|||
|
|
(question.minRating || 1) +
|
|||
|
|
1,
|
|||
|
|
},
|
|||
|
|
(_, i) => {
|
|||
|
|
const value =
|
|||
|
|
(question.minRating || 1) + i;
|
|||
|
|
return (
|
|||
|
|
<label
|
|||
|
|
key={value}
|
|||
|
|
className="flex flex-col items-center cursor-pointer"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`question-${question.id}`}
|
|||
|
|
value={value}
|
|||
|
|
checked={
|
|||
|
|
evaluationResponses[
|
|||
|
|
question.id
|
|||
|
|
] === value
|
|||
|
|
}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
Number(e.target.value)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mb-1"
|
|||
|
|
/>
|
|||
|
|
<span className="text-xs text-gray-600">
|
|||
|
|
{question.ratingLabels?.[i] ||
|
|||
|
|
value}
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.Text && (
|
|||
|
|
<textarea
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
rows={3}
|
|||
|
|
placeholder="Yorumunuzu yazın..."
|
|||
|
|
value={
|
|||
|
|
evaluationResponses[question.id] || ""
|
|||
|
|
}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
e.target.value
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.YesNo && (
|
|||
|
|
<div className="flex gap-4">
|
|||
|
|
<label className="flex items-center cursor-pointer">
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`question-${question.id}`}
|
|||
|
|
value="yes"
|
|||
|
|
checked={
|
|||
|
|
evaluationResponses[question.id] ===
|
|||
|
|
"yes"
|
|||
|
|
}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
e.target.value
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
Evet
|
|||
|
|
</label>
|
|||
|
|
<label className="flex items-center cursor-pointer">
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`question-${question.id}`}
|
|||
|
|
value="no"
|
|||
|
|
checked={
|
|||
|
|
evaluationResponses[question.id] ===
|
|||
|
|
"no"
|
|||
|
|
}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
e.target.value
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
Hayır
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.MultipleChoice && (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{question.options?.map((option) => (
|
|||
|
|
<label
|
|||
|
|
key={option.id}
|
|||
|
|
className="flex items-center cursor-pointer"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`question-${question.id}`}
|
|||
|
|
value={option.value}
|
|||
|
|
checked={
|
|||
|
|
evaluationResponses[
|
|||
|
|
question.id
|
|||
|
|
] === option.value
|
|||
|
|
}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
Number(e.target.value)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
{option.optionText}
|
|||
|
|
</label>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-end gap-2 p-2.5 border-t">
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="px-2.5 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
|
|||
|
|
>
|
|||
|
|
İptal
|
|||
|
|
</button>
|
|||
|
|
{evaluationTarget && evaluationEvaluator && (
|
|||
|
|
<button
|
|||
|
|
onClick={handleSaveEvaluation}
|
|||
|
|
className="flex items-center gap-2 px-2.5 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700"
|
|||
|
|
>
|
|||
|
|
<FaSave className="w-4 h-4" />
|
|||
|
|
Değerlendirmeyi Kaydet
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Result Detail Modal */}
|
|||
|
|
{showResultDetailModal && selectedResult && (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-2">
|
|||
|
|
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|||
|
|
<div className="flex justify-between items-center p-2.5 border-b">
|
|||
|
|
<h2 className="text-base font-semibold">Değerlendirme Detayı</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="p-1.5 text-gray-400 hover:text-gray-600"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-3 space-y-2.5">
|
|||
|
|
{/* Genel Bilgiler */}
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|||
|
|
<div className="bg-blue-50 p-4 rounded-lg">
|
|||
|
|
<h3 className="font-medium text-blue-800 mb-2 text-sm">
|
|||
|
|
Değerlendirme Bilgileri
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-1.5">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-blue-700 font-medium">
|
|||
|
|
Kampanya:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-blue-800">
|
|||
|
|
{selectedResult.campaignName}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-blue-700 font-medium">
|
|||
|
|
Değerlendirilen:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-blue-800">
|
|||
|
|
{selectedResult.evaluatedEmployeeName}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-blue-700 font-medium">
|
|||
|
|
Değerlendiren:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-blue-800">
|
|||
|
|
{selectedResult.evaluatorName}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-blue-700 font-medium">
|
|||
|
|
Değerlendiren Tipi:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-blue-800">
|
|||
|
|
{getAssessorTypeText(selectedResult.evaluatorType)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="bg-green-50 p-4 rounded-lg">
|
|||
|
|
<h3 className="font-medium text-green-800 mb-2 text-sm">
|
|||
|
|
Sonuç Bilgileri
|
|||
|
|
</h3>
|
|||
|
|
<div className="space-y-1.5">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-green-700 font-medium">
|
|||
|
|
Durum:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span
|
|||
|
|
className={`text-sm px-2 py-0.5 rounded-full ${getParticipantStatusColor(
|
|||
|
|
selectedResult.status
|
|||
|
|
)}`}
|
|||
|
|
>
|
|||
|
|
{getParticipantStatusText(selectedResult.status)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-green-700 font-medium">
|
|||
|
|
Toplam Puan:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-green-800 font-bold">
|
|||
|
|
{selectedResult.overallScore}/100
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-green-700 font-medium">
|
|||
|
|
Yüzde:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-green-800 font-bold">
|
|||
|
|
%{selectedResult.scorePercentage}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
{selectedResult.completedDate && (
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-green-700 font-medium">
|
|||
|
|
Tamamlanma Tarihi:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-green-800">
|
|||
|
|
{new Date(
|
|||
|
|
selectedResult.completedDate
|
|||
|
|
).toLocaleDateString("tr-TR")}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Detaylı Sonuçlar */}
|
|||
|
|
<div className="border rounded-lg p-3">
|
|||
|
|
<h3 className="font-medium text-gray-900 mb-2 text-sm">
|
|||
|
|
Detaylı Sonuçlar
|
|||
|
|
</h3>
|
|||
|
|
|
|||
|
|
{(() => {
|
|||
|
|
// Seçili sonucu mockEvaluation360Results'tan bul
|
|||
|
|
const evaluationResult = mockEvaluation360Results.find(
|
|||
|
|
(r) => r.id === selectedResult.resultId
|
|||
|
|
);
|
|||
|
|
if (!evaluationResult) {
|
|||
|
|
return (
|
|||
|
|
<div className="text-center py-8 text-gray-500">
|
|||
|
|
<FaChartBar className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|||
|
|
<p>Sonuç verisi bulunamadı</p>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-2.5">
|
|||
|
|
{/* Soru Cevapları */}
|
|||
|
|
{(() => {
|
|||
|
|
const campaign = mockEvaluation360.find(
|
|||
|
|
(c) => c.id === selectedResult.campaignId
|
|||
|
|
);
|
|||
|
|
const template = campaign
|
|||
|
|
? mockEvaluation360Templates.find(
|
|||
|
|
(t) => t.id === campaign.templateId
|
|||
|
|
)
|
|||
|
|
: null;
|
|||
|
|
|
|||
|
|
if (template) {
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
📝 Soru Cevapları
|
|||
|
|
</h4>
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
{template.questionGroups.map((group) => (
|
|||
|
|
<div
|
|||
|
|
key={group.id}
|
|||
|
|
className="bg-gray-50 p-2.5 rounded-lg"
|
|||
|
|
>
|
|||
|
|
<div className="mb-3">
|
|||
|
|
<h5 className="font-medium text-gray-800">
|
|||
|
|
{group.groupName}
|
|||
|
|
</h5>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{group.description}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2.5">
|
|||
|
|
{group.questions.map(
|
|||
|
|
(question, qIndex) => {
|
|||
|
|
const response =
|
|||
|
|
evaluationResult.participants
|
|||
|
|
.flatMap((p) => p.responses)
|
|||
|
|
.find(
|
|||
|
|
(r) =>
|
|||
|
|
r.questionId === question.id
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!response) return null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={question.id}
|
|||
|
|
className="bg-white p-1.5 rounded border-l-4 border-blue-200"
|
|||
|
|
>
|
|||
|
|
<div className="mb-2">
|
|||
|
|
<span className="text-sm font-medium text-gray-800">
|
|||
|
|
S{qIndex + 1}:{" "}
|
|||
|
|
{question.questionText}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<span className="text-sm text-gray-600">
|
|||
|
|
Cevap:
|
|||
|
|
</span>
|
|||
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-sm font-medium">
|
|||
|
|
{(() => {
|
|||
|
|
if (
|
|||
|
|
question.questionType ===
|
|||
|
|
QuestionTypeEnum.Rating
|
|||
|
|
) {
|
|||
|
|
const ratingIndex =
|
|||
|
|
Number(
|
|||
|
|
response.responseValue
|
|||
|
|
) -
|
|||
|
|
(question.minRating ||
|
|||
|
|
1);
|
|||
|
|
const label =
|
|||
|
|
question
|
|||
|
|
.ratingLabels?.[
|
|||
|
|
ratingIndex
|
|||
|
|
];
|
|||
|
|
return label
|
|||
|
|
? `${response.responseValue} - ${label}`
|
|||
|
|
: response.responseValue;
|
|||
|
|
} else if (
|
|||
|
|
question.questionType ===
|
|||
|
|
QuestionTypeEnum.YesNo
|
|||
|
|
) {
|
|||
|
|
return response.responseValue ===
|
|||
|
|
"yes"
|
|||
|
|
? "Evet"
|
|||
|
|
: "Hayır";
|
|||
|
|
} else if (
|
|||
|
|
question.questionType ===
|
|||
|
|
QuestionTypeEnum.MultipleChoice
|
|||
|
|
) {
|
|||
|
|
const option =
|
|||
|
|
question.options?.find(
|
|||
|
|
(o) =>
|
|||
|
|
o.value ===
|
|||
|
|
Number(
|
|||
|
|
response.responseValue
|
|||
|
|
)
|
|||
|
|
);
|
|||
|
|
return option
|
|||
|
|
? option.optionText
|
|||
|
|
: response.responseValue;
|
|||
|
|
} else {
|
|||
|
|
return response.responseValue;
|
|||
|
|
}
|
|||
|
|
})()}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<span className="text-xs text-gray-500">
|
|||
|
|
Puan:
|
|||
|
|
</span>
|
|||
|
|
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-sm font-bold">
|
|||
|
|
{response.score}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{response.responseText && (
|
|||
|
|
<div className="mt-2 p-2 bg-gray-100 rounded">
|
|||
|
|
<span className="text-xs text-gray-600">
|
|||
|
|
Detay:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-gray-700">
|
|||
|
|
{response.responseText}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
})()}
|
|||
|
|
|
|||
|
|
{/* Grup Bazlı Puanlar */}
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
📊 Grup Bazlı Puanlar
|
|||
|
|
</h4>
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|||
|
|
{evaluationResult.groupScores.map((groupScore) => (
|
|||
|
|
<div
|
|||
|
|
key={groupScore.groupId}
|
|||
|
|
className="bg-gray-50 p-2.5 rounded-lg"
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between items-center mb-2">
|
|||
|
|
<h5 className="font-medium text-gray-700">
|
|||
|
|
{groupScore.groupName}
|
|||
|
|
</h5>
|
|||
|
|
<span className="text-lg font-bold text-blue-600">
|
|||
|
|
{groupScore.score}/{groupScore.maxScore}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="w-full bg-gray-200 rounded-full h-2 mb-2">
|
|||
|
|
<div
|
|||
|
|
className="bg-blue-600 h-2 rounded-full"
|
|||
|
|
style={{ width: `${groupScore.percentage}%` }}
|
|||
|
|
></div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-between text-sm text-gray-600">
|
|||
|
|
<span>
|
|||
|
|
%{Math.round(groupScore.percentage)}
|
|||
|
|
</span>
|
|||
|
|
<span>{groupScore.responseCount} cevap</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Değerlendirici Tipi Bazlı Puanlar */}
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
👥 Değerlendirici Tipi Bazlı Puanlar
|
|||
|
|
</h4>
|
|||
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|||
|
|
{evaluationResult.assessorTypeScores.map(
|
|||
|
|
(assessorScore, index) => (
|
|||
|
|
<div
|
|||
|
|
key={index}
|
|||
|
|
className="bg-blue-50 p-2.5 rounded-lg text-center"
|
|||
|
|
>
|
|||
|
|
<div className="text-sm text-blue-700 font-medium mb-1">
|
|||
|
|
{getAssessorTypeText(
|
|||
|
|
assessorScore.assessorType
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-2xl font-bold text-blue-800 mb-1">
|
|||
|
|
%{Math.round(assessorScore.percentage)}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-xs text-blue-600">
|
|||
|
|
{assessorScore.averageScore}/
|
|||
|
|
{assessorScore.maxScore} puan
|
|||
|
|
</div>
|
|||
|
|
<div className="text-xs text-blue-500">
|
|||
|
|
{assessorScore.assessorCount} değerlendirici
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Güçlü Yönler */}
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
💪 Güçlü Yönler
|
|||
|
|
</h4>
|
|||
|
|
<div className="bg-green-50 p-2.5 rounded-lg">
|
|||
|
|
{evaluationResult.strengths.length > 0 ? (
|
|||
|
|
<ul className="list-disc list-inside space-y-1">
|
|||
|
|
{evaluationResult.strengths.map(
|
|||
|
|
(strength, index) => (
|
|||
|
|
<li key={index} className="text-green-800">
|
|||
|
|
{strength}
|
|||
|
|
</li>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</ul>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-green-700 italic">
|
|||
|
|
Güçlü yönler henüz analiz edilmedi
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Gelişim Alanları */}
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
📈 Gelişim Alanları
|
|||
|
|
</h4>
|
|||
|
|
<div className="bg-amber-50 p-2.5 rounded-lg">
|
|||
|
|
{evaluationResult.developmentAreas.length > 0 ? (
|
|||
|
|
<ul className="list-disc list-inside space-y-1">
|
|||
|
|
{evaluationResult.developmentAreas.map(
|
|||
|
|
(area, index) => (
|
|||
|
|
<li key={index} className="text-amber-800">
|
|||
|
|
{area}
|
|||
|
|
</li>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</ul>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-amber-700 italic">
|
|||
|
|
Gelişim alanları henüz analiz edilmedi
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Aksiyon Planı */}
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
🎯 Aksiyon Planı
|
|||
|
|
</h4>
|
|||
|
|
<div className="bg-purple-50 p-2.5 rounded-lg">
|
|||
|
|
{evaluationResult.actionPlan.length > 0 ? (
|
|||
|
|
<ul className="list-decimal list-inside space-y-1">
|
|||
|
|
{evaluationResult.actionPlan.map(
|
|||
|
|
(action, index) => (
|
|||
|
|
<li key={index} className="text-purple-800">
|
|||
|
|
{action}
|
|||
|
|
</li>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</ul>
|
|||
|
|
) : (
|
|||
|
|
<p className="text-purple-700 italic">
|
|||
|
|
Aksiyon planı henüz oluşturulmadı
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Yorumlar */}
|
|||
|
|
{(evaluationResult.managerComments ||
|
|||
|
|
evaluationResult.hrComments) && (
|
|||
|
|
<div>
|
|||
|
|
<h4 className="font-medium text-gray-800 mb-2 text-sm">
|
|||
|
|
💬 Yorumlar
|
|||
|
|
</h4>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{evaluationResult.managerComments && (
|
|||
|
|
<div className="bg-blue-50 p-2.5 rounded-lg">
|
|||
|
|
<h5 className="font-medium text-blue-800 mb-2">
|
|||
|
|
Yönetici Yorumu
|
|||
|
|
</h5>
|
|||
|
|
<p className="text-blue-700">
|
|||
|
|
{evaluationResult.managerComments}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{evaluationResult.hrComments && (
|
|||
|
|
<div className="bg-indigo-50 p-2.5 rounded-lg">
|
|||
|
|
<h5 className="font-medium text-indigo-800 mb-2">
|
|||
|
|
İK Yorumu
|
|||
|
|
</h5>
|
|||
|
|
<p className="text-indigo-700">
|
|||
|
|
{evaluationResult.hrComments}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-end gap-2 p-2.5 border-t">
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="px-2.5 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
|
|||
|
|
>
|
|||
|
|
Kapat
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
handleCloseModal();
|
|||
|
|
handleEditEvaluationDetail(selectedResult);
|
|||
|
|
}}
|
|||
|
|
className="flex items-center gap-2 px-2.5 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|||
|
|
>
|
|||
|
|
<FaEdit className="w-4 h-4" />
|
|||
|
|
Düzenle
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Result Edit Modal */}
|
|||
|
|
{showResultEditModal && selectedResult && (
|
|||
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-2">
|
|||
|
|
<div className="bg-white rounded-lg w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|||
|
|
<div className="flex justify-between items-center p-2.5 border-b">
|
|||
|
|
<h2 className="text-base font-semibold">Değerlendirme Düzenle</h2>
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="p-1.5 text-gray-400 hover:text-gray-600"
|
|||
|
|
>
|
|||
|
|
<FaTimes className="w-4 h-4" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="p-3 space-y-2.5">
|
|||
|
|
<div className="bg-amber-50 p-2.5 rounded-lg">
|
|||
|
|
<h3 className="font-medium text-amber-800 mb-2 text-sm">
|
|||
|
|
Değerlendirme Bilgileri
|
|||
|
|
</h3>
|
|||
|
|
<div className="grid grid-cols-2 gap-4">
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-amber-700 font-medium">
|
|||
|
|
Kampanya:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-amber-800">
|
|||
|
|
{selectedResult.campaignName}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-amber-700 font-medium">
|
|||
|
|
Değerlendirilen:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-amber-800">
|
|||
|
|
{selectedResult.evaluatedEmployeeName}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-amber-700 font-medium">
|
|||
|
|
Değerlendiren:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-amber-800">
|
|||
|
|
{selectedResult.evaluatorName}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<span className="text-sm text-amber-700 font-medium">
|
|||
|
|
Değerlendiren Tipi:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-sm text-amber-800">
|
|||
|
|
{getAssessorTypeText(selectedResult.evaluatorType)}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Soru Grupları ve Cevaplar */}
|
|||
|
|
{(() => {
|
|||
|
|
const evaluationResult = mockEvaluation360Results.find(
|
|||
|
|
(r) => r.id === selectedResult.resultId
|
|||
|
|
);
|
|||
|
|
const campaign = mockEvaluation360.find(
|
|||
|
|
(c) => c.id === selectedResult.campaignId
|
|||
|
|
);
|
|||
|
|
const template = campaign
|
|||
|
|
? mockEvaluation360Templates.find(
|
|||
|
|
(t) => t.id === campaign.templateId
|
|||
|
|
)
|
|||
|
|
: null;
|
|||
|
|
|
|||
|
|
if (!evaluationResult || !template) {
|
|||
|
|
return (
|
|||
|
|
<div className="text-center py-8 text-gray-500">
|
|||
|
|
<p>Template verisi bulunamadı</p>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-3">
|
|||
|
|
<h3 className="text-sm font-medium text-gray-900">
|
|||
|
|
Soru Grupları ve Cevaplar
|
|||
|
|
</h3>
|
|||
|
|
|
|||
|
|
{template.questionGroups.map((group) => (
|
|||
|
|
<div key={group.id} className="border rounded-lg p-2">
|
|||
|
|
<div className="mb-3">
|
|||
|
|
<h4 className="font-medium text-gray-900 text-sm">
|
|||
|
|
{group.groupName}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-sm text-gray-600">
|
|||
|
|
{group.description}
|
|||
|
|
</p>
|
|||
|
|
<div className="flex items-center gap-2 mt-2">
|
|||
|
|
<span className="text-xs text-gray-500">
|
|||
|
|
Ağırlık: %{group.weight}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xs text-gray-500">•</span>
|
|||
|
|
<span className="text-xs text-gray-500">
|
|||
|
|
{group.questions.length} soru
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="space-y-2.5">
|
|||
|
|
{group.questions.map((question, qIndex) => {
|
|||
|
|
const currentResponse =
|
|||
|
|
evaluationResult.participants
|
|||
|
|
.flatMap((p) => p.responses)
|
|||
|
|
.find((r) => r.questionId === question.id);
|
|||
|
|
|
|||
|
|
const editValue =
|
|||
|
|
editResponses[question.id] !== undefined
|
|||
|
|
? editResponses[question.id]
|
|||
|
|
: currentResponse?.responseValue;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
key={question.id}
|
|||
|
|
className="bg-gray-50 p-2.5 rounded-lg"
|
|||
|
|
>
|
|||
|
|
<p className="font-medium text-gray-800 mb-3">
|
|||
|
|
{qIndex + 1}. {question.questionText}
|
|||
|
|
{question.isRequired && (
|
|||
|
|
<span className="text-red-500 ml-1">*</span>
|
|||
|
|
)}
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
{currentResponse && (
|
|||
|
|
<div className="mb-2 p-1.5 bg-blue-50 rounded">
|
|||
|
|
<span className="text-xs text-blue-700 font-medium">
|
|||
|
|
Mevcut Cevap:{" "}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xs text-blue-800">
|
|||
|
|
{typeof currentResponse.responseValue ===
|
|||
|
|
"string"
|
|||
|
|
? currentResponse.responseValue
|
|||
|
|
: `${currentResponse.responseValue} puan`}
|
|||
|
|
</span>
|
|||
|
|
<span className="text-xs text-blue-600 ml-2">
|
|||
|
|
(Puan: {currentResponse.score})
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Soru tipine göre düzenleme alanları */}
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.Rating && (
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<label className="text-sm text-gray-700 font-medium">
|
|||
|
|
Yeni Puan:
|
|||
|
|
</label>
|
|||
|
|
<div className="flex gap-2.5">
|
|||
|
|
{Array.from(
|
|||
|
|
{
|
|||
|
|
length:
|
|||
|
|
(question.maxRating || 5) -
|
|||
|
|
(question.minRating || 1) +
|
|||
|
|
1,
|
|||
|
|
},
|
|||
|
|
(_, i) => {
|
|||
|
|
const value =
|
|||
|
|
(question.minRating || 1) + i;
|
|||
|
|
return (
|
|||
|
|
<label
|
|||
|
|
key={value}
|
|||
|
|
className="flex flex-col items-center cursor-pointer"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`edit-question-${question.id}`}
|
|||
|
|
value={value}
|
|||
|
|
checked={editValue === value}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleEditResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
Number(e.target.value)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mb-1"
|
|||
|
|
/>
|
|||
|
|
<span className="text-xs text-gray-600">
|
|||
|
|
{question.ratingLabels?.[i] ||
|
|||
|
|
value}
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.Text && (
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm text-gray-700 font-medium mb-2">
|
|||
|
|
Yeni Cevap:
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
rows={3}
|
|||
|
|
placeholder="Yeni cevabı yazın..."
|
|||
|
|
value={editValue || ""}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleEditResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
e.target.value
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.YesNo && (
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm text-gray-700 font-medium mb-2">
|
|||
|
|
Yeni Cevap:
|
|||
|
|
</label>
|
|||
|
|
<div className="flex gap-4">
|
|||
|
|
<label className="flex items-center cursor-pointer">
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`edit-question-${question.id}`}
|
|||
|
|
value="yes"
|
|||
|
|
checked={editValue === "yes"}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleEditResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
e.target.value
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
Evet
|
|||
|
|
</label>
|
|||
|
|
<label className="flex items-center cursor-pointer">
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`edit-question-${question.id}`}
|
|||
|
|
value="no"
|
|||
|
|
checked={editValue === "no"}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleEditResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
e.target.value
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
Hayır
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{question.questionType ===
|
|||
|
|
QuestionTypeEnum.MultipleChoice && (
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm text-gray-700 font-medium mb-2">
|
|||
|
|
Yeni Seçim:
|
|||
|
|
</label>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{question.options?.map((option) => (
|
|||
|
|
<label
|
|||
|
|
key={option.id}
|
|||
|
|
className="flex items-center cursor-pointer"
|
|||
|
|
>
|
|||
|
|
<input
|
|||
|
|
type="radio"
|
|||
|
|
name={`edit-question-${question.id}`}
|
|||
|
|
value={option.value}
|
|||
|
|
checked={editValue === option.value}
|
|||
|
|
onChange={(e) =>
|
|||
|
|
handleEditResponseChange(
|
|||
|
|
question.id,
|
|||
|
|
Number(e.target.value)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
className="mr-2"
|
|||
|
|
/>
|
|||
|
|
{option.optionText}
|
|||
|
|
</label>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})()}
|
|||
|
|
|
|||
|
|
{/* Yorumlar ve Durum */}
|
|||
|
|
<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">
|
|||
|
|
Yönetici Yorumları
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="Yönetici yorumlarını buraya yazın..."
|
|||
|
|
value={editManagerComments}
|
|||
|
|
onChange={(e) => setEditManagerComments(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
İK Yorumları
|
|||
|
|
</label>
|
|||
|
|
<textarea
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="İK yorumlarını buraya yazın..."
|
|||
|
|
value={editHrComments}
|
|||
|
|
onChange={(e) => setEditHrComments(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|||
|
|
Sonuç Durumu
|
|||
|
|
</label>
|
|||
|
|
<select
|
|||
|
|
className="w-full border border-gray-300 rounded-md px-2.5 py-1.5 text-sm"
|
|||
|
|
value={editResultStatus}
|
|||
|
|
onChange={(e) => setEditResultStatus(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="PENDING">Beklemede</option>
|
|||
|
|
<option value="IN_REVIEW">İncelemede</option>
|
|||
|
|
<option value="APPROVED">Onaylandı</option>
|
|||
|
|
<option value="PUBLISHED">Yayınlandı</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Mevcut Puan Bilgileri */}
|
|||
|
|
<div className="border rounded-lg p-3">
|
|||
|
|
<h4 className="font-medium text-gray-900 mb-2 text-sm">
|
|||
|
|
Mevcut Puan Bilgileri
|
|||
|
|
</h4>
|
|||
|
|
<div className="grid grid-cols-2 gap-3">
|
|||
|
|
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
|||
|
|
<div className="text-2xl font-bold text-blue-600">
|
|||
|
|
{selectedResult.overallScore}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-blue-700">Toplam Puan</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="text-center p-4 bg-green-50 rounded-lg">
|
|||
|
|
<div className="text-2xl font-bold text-green-600">
|
|||
|
|
%{selectedResult.scorePercentage}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-green-700">Başarı Yüzdesi</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div className="flex justify-end gap-2 p-2.5 border-t">
|
|||
|
|
<button
|
|||
|
|
onClick={handleCloseModal}
|
|||
|
|
className="px-2.5 py-1 text-sm text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
|
|||
|
|
>
|
|||
|
|
İptal
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
console.log("Güncellenen cevaplar:", editResponses);
|
|||
|
|
console.log("Yönetici yorumları:", editManagerComments);
|
|||
|
|
console.log("İK yorumları:", editHrComments);
|
|||
|
|
console.log("Durum:", editResultStatus);
|
|||
|
|
alert("Değerlendirme güncellendi!");
|
|||
|
|
handleCloseModal();
|
|||
|
|
}}
|
|||
|
|
className="flex items-center gap-2 px-2.5 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700"
|
|||
|
|
>
|
|||
|
|
<FaSave className="w-4 h-4" />
|
|||
|
|
Kaydet
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default Degree360Evaluation;
|