erp-platform/ui/src/views/hr/components/Degree360Templates.tsx
2025-09-17 12:46:58 +03:00

781 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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