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

863 lines
31 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
import React, { useState } from "react";
import {
FaEye,
FaEdit,
FaPlus,
FaTimes,
FaSave,
FaTrash,
FaQuestionCircle,
} from "react-icons/fa";
import {
HrEvaluation360Template,
HrQuestionGroup,
HrEvaluationQuestion,
QuestionTypeEnum,
HrQuestionOption,
AssessorTypeEnum,
} from "../../../types/hr";
import DataTable, { Column } from "../../../components/common/DataTable";
import { mockEvaluation360Templates } from "../../../mocks/mockEvaluation360Templates";
import Widget from "../../../components/common/Widget";
const Degree360Templates: React.FC = () => {
const [templateSearchTerm, setTemplateSearchTerm] = useState("");
// Modal states
const [showTemplateModal, setShowTemplateModal] = useState(false);
const [showQuestionModal, setShowQuestionModal] = useState(false);
const [showQuestionItemModal, setShowQuestionItemModal] = useState(false);
const [selectedTemplate, setSelectedTemplate] =
useState<HrEvaluation360Template | null>(null);
const [selectedQuestionGroup, setSelectedQuestionGroup] =
useState<HrQuestionGroup | null>(null);
const [selectedQuestion, setSelectedQuestion] =
useState<HrEvaluationQuestion | null>(null);
const [isEditMode, setIsEditMode] = useState(false);
// Form states
const [templateForm, setTemplateForm] = useState({
name: "",
description: "",
isActive: true,
assessorTypes: [] as AssessorTypeEnum[],
});
const [questionGroupForm, setQuestionGroupForm] = useState({
groupName: "",
description: "",
weight: 0,
order: 0,
});
const [questionForm, setQuestionForm] = useState({
questionText: "",
questionType: QuestionTypeEnum.Rating,
isRequired: true,
order: 0,
weight: 0,
options: [] as HrQuestionOption[],
});
// Filter templates
const filteredTemplates = mockEvaluation360Templates.filter((template) => {
const searchLower = templateSearchTerm.toLowerCase();
return (
template.name.toLowerCase().includes(searchLower) ||
template.description.toLowerCase().includes(searchLower)
);
});
// Statistics
const totalTemplates = mockEvaluation360Templates.length;
const activeTemplates = mockEvaluation360Templates.filter(
(t) => t.isActive
).length;
// Event handlers
const handleNewTemplate = () => {
setTemplateForm({
name: "",
description: "",
isActive: true,
assessorTypes: [],
});
setSelectedTemplate(null);
setIsEditMode(false);
setShowTemplateModal(true);
};
const handleEditTemplate = (template: HrEvaluation360Template) => {
setTemplateForm({
name: template.name,
description: template.description,
isActive: template.isActive,
assessorTypes: template.assessorTypes || [],
});
setSelectedTemplate(template);
setIsEditMode(true);
setShowTemplateModal(true);
};
const handleViewTemplate = (template: HrEvaluation360Template) => {
setTemplateForm({
name: template.name,
description: template.description,
isActive: template.isActive,
assessorTypes: template.assessorTypes || [],
});
setSelectedTemplate(template);
setShowTemplateModal(true);
setIsEditMode(false);
};
const handleSaveTemplate = () => {
console.log("Saving template:", templateForm);
setShowTemplateModal(false);
};
const handleDeleteTemplate = (template: HrEvaluation360Template) => {
if (
confirm(
`"${template.name}" şablonunu silmek istediğinizden emin misiniz?`
)
) {
console.log("Deleting template:", template.id);
}
};
const handleAddQuestionGroup = () => {
setQuestionGroupForm({
groupName: "",
description: "",
weight: 0,
order: 0,
});
setSelectedQuestionGroup(null);
setShowQuestionModal(true);
};
const handleEditQuestionGroup = (group: HrQuestionGroup) => {
setQuestionGroupForm({
groupName: group.groupName,
description: group.description || "",
weight: group.weight,
order: group.order,
});
setSelectedQuestionGroup(group);
setShowQuestionModal(true);
};
const handleSaveQuestionGroup = () => {
console.log("Saving question group:", questionGroupForm);
setShowQuestionModal(false);
};
const handleAddQuestion = (group: HrQuestionGroup) => {
setQuestionForm({
questionText: "",
questionType: QuestionTypeEnum.Rating,
isRequired: true,
order: 0,
weight: 0,
options: [],
});
setSelectedQuestion(null);
setSelectedQuestionGroup(group);
setShowQuestionItemModal(true);
};
const handleEditQuestion = (
question: HrEvaluationQuestion,
group: HrQuestionGroup
) => {
setQuestionForm({
questionText: question.questionText,
questionType: question.questionType,
isRequired: question.isRequired,
order: question.order,
weight: question.weight,
options: question.options || [],
});
setSelectedQuestion(question);
setSelectedQuestionGroup(group);
setShowQuestionItemModal(true);
};
const handleSaveQuestion = () => {
console.log("Saving question:", questionForm);
setShowQuestionItemModal(false);
};
// Table columns
const templateColumns: Column<HrEvaluation360Template>[] = [
{
key: "name",
header: "Şablon Adı",
sortable: true,
render: (template) => (
<div>
<div className="font-medium text-gray-900">{template.name}</div>
<div className="text-sm text-gray-500">{template.description}</div>
</div>
),
},
{
key: "questionGroups",
header: "Soru Grupları",
render: (template) => (
<span className="text-sm text-gray-600">
{template.questionGroups?.length || 0} grup
</span>
),
},
{
key: "assessorTypes",
header: "Değerlendirecekler",
render: (template) => {
const assessorLabels = {
[AssessorTypeEnum.Self]: "Kendi",
[AssessorTypeEnum.Manager]: "Yönetici",
[AssessorTypeEnum.Peer]: "Meslektaş",
[AssessorTypeEnum.Subordinate]: "Ast",
[AssessorTypeEnum.Customer]: "Müşteri",
[AssessorTypeEnum.OtherDepartment]: "Diğer Departman",
[AssessorTypeEnum.HRUpperManagement]: "İK/Üst Yönetim",
[AssessorTypeEnum.External]: "Dış Paydaş",
};
const assessorTypes = template.assessorTypes || [];
return (
<div className="flex flex-wrap gap-1">
{assessorTypes.map((type) => (
<span
key={type}
className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{assessorLabels[type] || type}
</span>
))}
{assessorTypes.length === 0 && (
<span className="text-sm text-gray-400">Belirtilmemiş</span>
)}
</div>
);
},
},
{
key: "isActive",
header: "Durum",
render: (template) => (
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
template.isActive
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{template.isActive ? "Aktif" : "Pasif"}
</span>
),
},
{
key: "actions",
header: "İşlemler",
render: (template) => (
<div className="flex items-center gap-2">
<button
onClick={() => handleViewTemplate(template)}
className="text-blue-600 hover:text-blue-900"
title="Görüntüle"
>
<FaEye className="w-4 h-4" />
</button>
<button
onClick={() => handleEditTemplate(template)}
className="text-green-600 hover:text-green-900"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDeleteTemplate(template)}
className="text-red-600 hover:text-red-900"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
),
},
];
return (
<div className="space-y-4 mt-2">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-gray-900">360° Şablonlar</h2>
<p className="text-sm text-gray-600 mt-1">
360 derece değerlendirme şablonlarını yönetin
</p>
</div>
<button
onClick={handleNewTemplate}
className="flex items-center gap-2 px-3 py-1.5 bg-purple-600 text-white rounded-md hover:bg-purple-700 ml-4 text-sm"
>
<FaPlus className="w-4 h-4" />
Yeni Şablon
</button>
</div>
{/* Statistics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Widget
title="Toplam Şablon"
value={totalTemplates}
color="purple"
icon="FaClipboardList"
/>
<Widget
title="Aktif Şablon"
value={activeTemplates}
color="green"
icon="FaClipboardList"
/>
<Widget
title="Pasif Şablon"
value={totalTemplates - activeTemplates}
color="red"
icon="FaClipboardList"
/>
</div>
<div className="bg-white p-4 rounded-lg border">
<div className="flex-1">
<input
type="text"
placeholder="Şablon adı veya açıklama ile ara..."
value={templateSearchTerm}
onChange={(e) => setTemplateSearchTerm(e.target.value)}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
{/* Templates Table */}
<DataTable data={filteredTemplates} columns={templateColumns} />
{/* Template Modal */}
{showTemplateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">
{isEditMode
? "Şablon Düzenle"
: selectedTemplate
? "Şablon Detayı"
: "Yeni Şablon"}
</h2>
<button
onClick={() => setShowTemplateModal(false)}
className="p-2 text-gray-500 hover:text-gray-700"
>
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
{/* Basic Information */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Şablon Adı
</label>
<input
type="text"
value={templateForm.name}
onChange={(e) =>
setTemplateForm({ ...templateForm, name: e.target.value })
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
disabled={!isEditMode && !!selectedTemplate}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Durum
</label>
<select
value={templateForm.isActive ? "active" : "inactive"}
onChange={(e) =>
setTemplateForm({
...templateForm,
isActive: e.target.value === "active",
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
disabled={!isEditMode && !!selectedTemplate}
>
<option value="active">Aktif</option>
<option value="inactive">Pasif</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ıklama
</label>
<textarea
value={templateForm.description}
onChange={(e) =>
setTemplateForm({
...templateForm,
description: e.target.value,
})
}
rows={3}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
disabled={!isEditMode && !!selectedTemplate}
/>
</div>
{/* Değerlendiriciler */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Değerlendirecekler
</label>
<div className="grid grid-cols-2 gap-3">
{Object.values(AssessorTypeEnum).map((type) => {
const labels = {
[AssessorTypeEnum.Self]: "Kendi",
[AssessorTypeEnum.Manager]: "Yönetici",
[AssessorTypeEnum.Peer]: "Meslektaş",
[AssessorTypeEnum.Subordinate]: "Ast",
[AssessorTypeEnum.Customer]: "Müşteri",
[AssessorTypeEnum.OtherDepartment]: "Diğer Departman",
[AssessorTypeEnum.HRUpperManagement]: "İK/Üst Yönetim",
[AssessorTypeEnum.External]: "Dış Paydaş",
};
return (
<label key={type} className="flex items-center space-x-2">
<input
type="checkbox"
checked={templateForm.assessorTypes.includes(type)}
onChange={(e) => {
if (e.target.checked) {
setTemplateForm({
...templateForm,
assessorTypes: [
...templateForm.assessorTypes,
type,
],
});
} else {
setTemplateForm({
...templateForm,
assessorTypes:
templateForm.assessorTypes.filter(
(t) => t !== type
),
});
}
}}
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
disabled={!isEditMode && !!selectedTemplate}
/>
<span className="text-sm text-gray-700">
{labels[type] || type}
</span>
</label>
);
})}
</div>
</div>
{/* Question Groups */}
{selectedTemplate && (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">Soru Grupları</h3>
{isEditMode && (
<button
onClick={handleAddQuestionGroup}
className="flex items-center gap-2 px-2 py-1 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-xs"
>
<FaPlus className="w-3 h-3" />
Grup Ekle
</button>
)}
</div>
<div className="space-y-3">
{selectedTemplate.questionGroups?.map(
(group, groupIndex) => (
<div
key={groupIndex}
className="bg-gray-50 p-3 rounded-lg"
>
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-medium">{group.groupName}</h4>
<p className="text-sm text-gray-600">
{group.description}
</p>
<p className="text-sm text-gray-600">
ırlık: {group.weight}%
</p>
</div>
{isEditMode && (
<div className="flex gap-2">
<button
onClick={() => handleEditQuestionGroup(group)}
className="text-blue-600 hover:text-blue-900"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleAddQuestion(group)}
className="text-green-600 hover:text-green-900"
>
<FaQuestionCircle className="w-4 h-4" />
</button>
</div>
)}
</div>
{/* Questions */}
<div className="space-y-2">
{group.questions?.map((question, questionIndex) => (
<div
key={questionIndex}
className="bg-white p-2 rounded border"
>
<div className="flex justify-between items-start">
<div className="flex-1">
<p className="text-sm">
{question.questionText}
</p>
<div className="flex gap-4 mt-1">
<span className="text-xs text-gray-500">
Tip:{" "}
{question.questionType ===
QuestionTypeEnum.Rating
? "Puanlama"
: question.questionType ===
QuestionTypeEnum.MultipleChoice
? "Çoktan Seçmeli"
: question.questionType ===
QuestionTypeEnum.Text
? "Metin"
: "Evet/Hayır"}
</span>
<span className="text-xs text-gray-500">
ırlık: {question.weight}%
</span>
{question.isRequired && (
<span className="text-xs text-red-500">
Zorunlu
</span>
)}
</div>
</div>
{isEditMode && (
<button
onClick={() =>
handleEditQuestion(question, group)
}
className="text-blue-600 hover:text-blue-900"
>
<FaEdit className="w-4 h-4" />
</button>
)}
</div>
</div>
))}
</div>
</div>
)
)}
</div>
</div>
)}
</div>
<div className="flex justify-end gap-4 mt-4">
<button
onClick={() => setShowTemplateModal(false)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
{selectedTemplate && !isEditMode ? "Kapat" : "İptal"}
</button>
{(isEditMode || !selectedTemplate) && (
<button
onClick={handleSaveTemplate}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
>
<FaSave className="w-4 h-4" />
Kaydet
</button>
)}
</div>
</div>
</div>
)}
{/* Question Group Modal */}
{showQuestionModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">
{selectedQuestionGroup
? "Soru Grubu Düzenle"
: "Yeni Soru Grubu"}
</h2>
<button
onClick={() => setShowQuestionModal(false)}
className="p-2 text-gray-500 hover:text-gray-700"
>
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Grup Adı
</label>
<input
type="text"
value={questionGroupForm.groupName}
onChange={(e) =>
setQuestionGroupForm({
...questionGroupForm,
groupName: e.target.value,
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ıklama
</label>
<textarea
value={questionGroupForm.description}
onChange={(e) =>
setQuestionGroupForm({
...questionGroupForm,
description: e.target.value,
})
}
rows={3}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ırlık (%)
</label>
<input
type="number"
min="0"
max="100"
value={questionGroupForm.weight}
onChange={(e) =>
setQuestionGroupForm({
...questionGroupForm,
weight: Number(e.target.value),
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sıra
</label>
<input
type="number"
min="0"
value={questionGroupForm.order}
onChange={(e) =>
setQuestionGroupForm({
...questionGroupForm,
order: Number(e.target.value),
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
</div>
<div className="flex justify-end gap-4 mt-4">
<button
onClick={() => setShowQuestionModal(false)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
İptal
</button>
<button
onClick={handleSaveQuestionGroup}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
>
<FaSave className="w-4 h-4" />
Kaydet
</button>
</div>
</div>
</div>
)}
{/* Question Modal */}
{showQuestionItemModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">
{selectedQuestion ? "Soru Düzenle" : "Yeni Soru"}
</h2>
<button
onClick={() => setShowQuestionItemModal(false)}
className="p-2 text-gray-500 hover:text-gray-700"
>
<FaTimes className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Soru Metni
</label>
<textarea
value={questionForm.questionText}
onChange={(e) =>
setQuestionForm({
...questionForm,
questionText: e.target.value,
})
}
rows={3}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Soru Tipi
</label>
<select
value={questionForm.questionType}
onChange={(e) =>
setQuestionForm({
...questionForm,
questionType: e.target.value as QuestionTypeEnum,
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value={QuestionTypeEnum.Rating}>Puanlama</option>
<option value={QuestionTypeEnum.MultipleChoice}>
Çoktan Seçmeli
</option>
<option value={QuestionTypeEnum.Text}>Metin</option>
<option value={QuestionTypeEnum.YesNo}>Evet/Hayır</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
ırlık (%)
</label>
<input
type="number"
min="0"
max="100"
value={questionForm.weight}
onChange={(e) =>
setQuestionForm({
...questionForm,
weight: Number(e.target.value),
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Sıra
</label>
<input
type="number"
min="0"
value={questionForm.order}
onChange={(e) =>
setQuestionForm({
...questionForm,
order: Number(e.target.value),
})
}
className="w-full border border-gray-300 rounded-md px-3 py-1.5 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="isRequired"
checked={questionForm.isRequired}
onChange={(e) =>
setQuestionForm({
...questionForm,
isRequired: e.target.checked,
})
}
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
/>
<label
htmlFor="isRequired"
className="ml-2 block text-sm text-gray-700"
>
Zorunlu soru
</label>
</div>
</div>
<div className="flex justify-end gap-4 mt-4">
<button
onClick={() => setShowQuestionItemModal(false)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
>
İptal
</button>
<button
onClick={handleSaveQuestion}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-600 text-white rounded-md hover:bg-purple-700"
>
<FaSave className="w-4 h-4" />
Kaydet
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Degree360Templates;