Hr Survey güncellemesi

This commit is contained in:
Sedat Öztürk 2025-10-25 22:43:36 +03:00
parent 68482e0a8d
commit f02136d358
5 changed files with 523 additions and 108 deletions

View file

@ -11,6 +11,10 @@ import {
MealMenu,
ShuttleRoute,
Survey,
SurveyQuestion,
SurveyQuestionOption,
SurveyResponse,
SurveyAnswer,
SocialPost,
} from '@/types/intranet'
@ -70,7 +74,58 @@ export const mockSurveys: Survey[] = [
creatorId: mockEmployees[0],
creationTime: new Date('2024-10-01'),
deadline: new Date('2024-10-31'),
totalQuestions: 25,
questions: [
{
id: 'q1',
surveyId: 'survey1',
questionText: 'Genel memnuniyet düzeyiniz nedir?',
type: 'rating',
order: 1,
isRequired: true,
ratingConfig: {
min: 1,
max: 5,
labels: {
1: 'Çok Kötü',
2: 'Kötü',
3: 'Orta',
4: 'İyi',
5: 'Çok İyi'
}
}
},
{
id: 'q2',
surveyId: 'survey1',
questionText: 'Hangi departmanda çalışıyorsunuz?',
type: 'multiple-choice',
order: 2,
isRequired: true,
options: [
{ id: 'opt1', text: 'Bilgi Teknolojileri', order: 1 },
{ id: 'opt2', text: 'İnsan Kaynakları', order: 2 },
{ id: 'opt3', text: 'Finans', order: 3 },
{ id: 'opt4', text: 'Satış', order: 4 },
{ id: 'opt5', text: 'Pazarlama', order: 5 }
]
},
{
id: 'q3',
surveyId: 'survey1',
questionText: 'Görüş ve önerileriniz',
type: 'textarea',
order: 3,
isRequired: false
},
{
id: 'q4',
surveyId: 'survey1',
questionText: 'Çalışma ortamından memnun musunuz?',
type: 'yes-no',
order: 4,
isRequired: true
}
],
responses: 45,
targetAudience: ['Tüm Çalışanlar'],
status: 'active',
@ -83,7 +138,53 @@ export const mockSurveys: Survey[] = [
creatorId: mockEmployees[2],
creationTime: new Date('2024-10-10'),
deadline: new Date('2024-11-15'),
totalQuestions: 15,
questions: [
{
id: 'q5',
surveyId: 'survey2',
questionText: 'Hangi teknoloji konularında eğitim almak istiyorsunuz?',
type: 'multiple-choice',
order: 1,
isRequired: true,
options: [
{ id: 'opt6', text: 'React / Frontend', order: 1 },
{ id: 'opt7', text: 'Node.js / Backend', order: 2 },
{ id: 'opt8', text: 'Database / SQL', order: 3 },
{ id: 'opt9', text: 'DevOps / Cloud', order: 4 },
{ id: 'opt10', text: 'Mobile Development', order: 5 }
]
},
{
id: 'q6',
surveyId: 'survey2',
questionText: 'Eğitim formatı tercihiniz nedir?',
type: 'multiple-choice',
order: 2,
isRequired: true,
options: [
{ id: 'opt11', text: 'Online Eğitim', order: 1 },
{ id: 'opt12', text: 'Yüz Yüze Eğitim', order: 2 },
{ id: 'opt13', text: 'Hibrit (Karma)', order: 3 }
]
},
{
id: 'q7',
surveyId: 'survey2',
questionText: 'Eğitim için haftalık ne kadar zaman ayırabilirsiniz?',
type: 'rating',
order: 3,
isRequired: true,
ratingConfig: {
min: 1,
max: 10,
labels: {
1: '1 saat',
5: '5 saat',
10: '10+ saat'
}
}
}
],
responses: 28,
targetAudience: ['Yazılım Geliştirme', 'Ürün Yönetimi'],
status: 'active',
@ -96,7 +197,43 @@ export const mockSurveys: Survey[] = [
creatorId: mockEmployees[4],
creationTime: new Date('2024-09-15'),
deadline: new Date('2024-09-30'),
totalQuestions: 10,
questions: [
{
id: 'q8',
surveyId: 'survey3',
questionText: 'Yemek kalitesini nasıl değerlendiriyorsunuz?',
type: 'rating',
order: 1,
isRequired: true,
ratingConfig: {
min: 1,
max: 5,
labels: {
1: 'Çok Kötü',
2: 'Kötü',
3: 'Orta',
4: 'İyi',
5: 'Mükemmel'
}
}
},
{
id: 'q9',
surveyId: 'survey3',
questionText: 'Hangi yemekleri daha sık görmek istiyorsunuz?',
type: 'textarea',
order: 2,
isRequired: false
},
{
id: 'q10',
surveyId: 'survey3',
questionText: 'Servis hızından memnun musunuz?',
type: 'yes-no',
order: 3,
isRequired: true
}
],
responses: 62,
targetAudience: ['Tüm Çalışanlar'],
status: 'closed',

View file

@ -161,6 +161,29 @@ export interface ShuttleRoute {
type: 'morning' | 'evening'
}
// Anket Sorusu
export interface SurveyQuestion {
id: string
surveyId: string
questionText: string
type: 'rating' | 'multiple-choice' | 'text' | 'textarea' | 'yes-no'
order: number
isRequired: boolean
options?: SurveyQuestionOption[]
ratingConfig?: {
min: number
max: number
labels?: { [key: number]: string }
}
}
// Anket Sorusu Seçeneği
export interface SurveyQuestionOption {
id: string
text: string
order: number
}
// Anket
export interface Survey {
id: string
@ -169,13 +192,29 @@ export interface Survey {
creatorId: HrEmployee
creationTime: Date
deadline: Date
totalQuestions: number
questions: SurveyQuestion[]
responses: number
targetAudience: string[]
status: 'draft' | 'active' | 'closed'
isAnonymous: boolean
}
// Anket Cevabı
export interface SurveyResponse {
id: string
surveyId: string
respondentId?: string // Anonymous ise null
submissionTime: Date
answers: SurveyAnswer[]
}
// Anket Cevap
export interface SurveyAnswer {
questionId: string
questionType: 'rating' | 'multiple-choice' | 'text' | 'textarea' | 'yes-no'
value: string | number | string[]
}
// Ziyaretçi
export interface Visitor {
id: string

View file

@ -31,7 +31,7 @@ import AnnouncementDetailModal from './modals/AnnouncementDetailModal'
// Social Wall
import SocialWall from './SocialWall'
import { Announcement, Survey } from '@/types/intranet'
import { Announcement, Survey, SurveyAnswer } from '@/types/intranet'
import { Container } from '@/components/shared'
dayjs.locale('tr')
@ -52,7 +52,9 @@ const IntranetDashboard: React.FC = () => {
setShowSurveyModal(true)
}
const handleSubmitSurvey = () => {
const handleSubmitSurvey = (answers: SurveyAnswer[]) => {
console.log('Survey submitted with answers:', answers)
// Burada survey cevapları API'ye gönderilecek
setShowSurveyModal(false)
setSelectedSurvey(null)
}

View file

@ -1,15 +1,214 @@
import React from 'react'
import React, { useState } from 'react'
import { motion } from 'framer-motion'
import { FaTimes } from 'react-icons/fa'
import { Survey } from '@/types/intranet'
import { Survey, SurveyQuestion, SurveyAnswer } from '@/types/intranet'
interface SurveyModalProps {
survey: Survey
onClose: () => void
onSubmit: () => void
onSubmit: (answers: SurveyAnswer[]) => void
}
const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit }) => {
const [answers, setAnswers] = useState<{ [questionId: string]: any }>({})
const [errors, setErrors] = useState<{ [questionId: string]: string }>({})
const handleAnswerChange = (questionId: string, value: any) => {
setAnswers(prev => ({
...prev,
[questionId]: value
}))
// Clear error when user provides an answer
if (errors[questionId]) {
setErrors(prev => ({
...prev,
[questionId]: ''
}))
}
}
const validateAnswers = (): boolean => {
const newErrors: { [questionId: string]: string } = {}
survey.questions.forEach(question => {
if (question.isRequired && (!answers[question.id] || answers[question.id] === '')) {
newErrors[question.id] = 'Bu alan zorunludur'
}
})
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!validateAnswers()) {
return
}
const surveyAnswers: SurveyAnswer[] = survey.questions.map(question => ({
questionId: question.id,
questionType: question.type,
value: answers[question.id] || (question.type === 'multiple-choice' ? '' :
question.type === 'rating' ? 0 : '')
}))
onSubmit(surveyAnswers)
}
const renderQuestion = (question: SurveyQuestion, index: number) => {
const questionNumber = index + 1
const hasError = !!errors[question.id]
switch (question.type) {
case 'rating':
return (
<div key={question.id} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{questionNumber}. {question.questionText} {question.isRequired && '*'}
</label>
<div className="flex gap-2 flex-wrap">
{Array.from({ length: question.ratingConfig!.max - question.ratingConfig!.min + 1 }, (_, i) => {
const value = question.ratingConfig!.min + i
const label = question.ratingConfig!.labels?.[value] || value.toString()
return (
<label
key={value}
className={`flex flex-col items-center gap-1 px-3 py-2 border rounded-lg cursor-pointer transition-colors ${
answers[question.id] === value
? 'bg-blue-100 border-blue-500 dark:bg-blue-900/30 dark:border-blue-400'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<input
type="radio"
name={`question-${question.id}`}
value={value}
checked={answers[question.id] === value}
onChange={(e) => handleAnswerChange(question.id, parseInt(e.target.value))}
className="sr-only"
/>
<span className="font-medium text-gray-900 dark:text-white">{value}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 text-center">{label}</span>
</label>
)
})}
</div>
{hasError && <p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>}
</div>
)
case 'multiple-choice':
return (
<div key={question.id} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{questionNumber}. {question.questionText} {question.isRequired && '*'}
</label>
<div className="space-y-2">
{question.options?.map(option => (
<label
key={option.id}
className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition-colors ${
answers[question.id] === option.id
? 'bg-blue-50 border-blue-500 dark:bg-blue-900/20 dark:border-blue-400'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<input
type="radio"
name={`question-${question.id}`}
value={option.id}
checked={answers[question.id] === option.id}
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
className="w-4 h-4 text-blue-600"
/>
<span className="text-sm text-gray-900 dark:text-white">{option.text}</span>
</label>
))}
</div>
{hasError && <p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>}
</div>
)
case 'yes-no':
return (
<div key={question.id} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{questionNumber}. {question.questionText} {question.isRequired && '*'}
</label>
<div className="flex gap-4">
{['yes', 'no'].map(value => (
<label
key={value}
className={`flex items-center gap-2 px-4 py-2 border rounded-lg cursor-pointer transition-colors ${
answers[question.id] === value
? 'bg-blue-50 border-blue-500 dark:bg-blue-900/20 dark:border-blue-400'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
<input
type="radio"
name={`question-${question.id}`}
value={value}
checked={answers[question.id] === value}
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
className="w-4 h-4 text-blue-600"
/>
<span className="text-sm text-gray-900 dark:text-white">
{value === 'yes' ? 'Evet' : 'Hayır'}
</span>
</label>
))}
</div>
{hasError && <p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>}
</div>
)
case 'text':
return (
<div key={question.id} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{questionNumber}. {question.questionText} {question.isRequired && '*'}
</label>
<input
type="text"
value={answers[question.id] || ''}
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
className={`w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 ${
hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="Cevabınızı yazın..."
/>
{hasError && <p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>}
</div>
)
case 'textarea':
return (
<div key={question.id} className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{questionNumber}. {question.questionText} {question.isRequired && '*'}
</label>
<textarea
rows={4}
value={answers[question.id] || ''}
onChange={(e) => handleAnswerChange(question.id, e.target.value)}
className={`w-full px-4 py-2 border rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 ${
hasError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'
}`}
placeholder="Yorumlarınızı buraya yazabilirsiniz..."
/>
{hasError && <p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>}
</div>
)
default:
return null
}
}
return (
<>
<motion.div
@ -44,58 +243,11 @@ const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit })
</button>
</div>
<form
onSubmit={(e) => {
e.preventDefault()
onSubmit()
}}
className="p-6 space-y-6"
>
{/* Örnek Anket Soruları */}
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
1. Genel memnuniyet düzeyiniz nedir? *
</label>
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((rating) => (
<label
key={rating}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700"
>
<input type="radio" name="rating" value={rating} required />
<span className="text-sm text-gray-900 dark:text-white">{rating}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
2. Hangi departmanda çalışıyorsunuz? *
</label>
<select
required
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
>
<option value="">Seçiniz</option>
<option value="it">Bilgi Teknolojileri</option>
<option value="hr">İnsan Kaynakları</option>
<option value="finance">Finans</option>
<option value="sales">Satış</option>
<option value="marketing">Pazarlama</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
3. Görüş ve önerileriniz
</label>
<textarea
rows={4}
placeholder="Yorumlarınızı buraya yazabilirsiniz..."
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
/>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div className="space-y-6">
{survey.questions
.sort((a, b) => a.order - b.order)
.map((question, index) => renderQuestion(question, index))}
</div>
{!survey.isAnonymous && (
@ -114,7 +266,6 @@ const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit })
</p>
</div>
)}
</div>
<div className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button

View file

@ -1,5 +1,5 @@
import React from 'react'
import { FaClipboardCheck } from 'react-icons/fa'
import { FaClipboardCheck, FaQuestionCircle, FaUsers, FaClock, FaArrowRight } from 'react-icons/fa'
import dayjs from 'dayjs'
import { mockSurveys } from '../../../mocks/mockIntranet'
import { Survey } from '@/types/intranet'
@ -9,47 +9,133 @@ interface ActiveSurveysProps {
}
const ActiveSurveys: React.FC<ActiveSurveysProps> = ({ onTakeSurvey }) => {
const activeSurveys = mockSurveys.filter((s) => s.status === 'active').slice(0, 3)
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="bg-gradient-to-br from-white to-gray-50 dark:from-gray-800 dark:to-gray-850 rounded-xl shadow-lg border border-gray-200/50 dark:border-gray-700/50 overflow-hidden">
{/* Header with gradient */}
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<FaClipboardCheck className="w-5 h-5" />
Aktif Anketler
</h2>
</div>
<div className="p-4 space-y-3">
{mockSurveys
.filter((s) => s.status === 'active')
.slice(0, 3)
.map((survey) => (
<div className="p-6 space-y-4">
{activeSurveys.map((survey, index) => {
const daysLeft = dayjs(survey.deadline).diff(dayjs(), 'day')
const urgency = daysLeft <= 3 ? 'urgent' : daysLeft <= 7 ? 'warning' : 'normal'
return (
<div
key={survey.id}
onClick={() => onTakeSurvey(survey)}
className="p-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 hover:bg-purple-100 dark:hover:bg-purple-900/30 cursor-pointer transition-colors"
className="group relative p-5 rounded-xl bg-white dark:bg-gray-750 border border-gray-200 dark:border-gray-600 hover:border-purple-300 dark:hover:border-purple-500 cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1"
>
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-1">
{/* Background gradient on hover */}
<div className="absolute inset-0 bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900/10 dark:to-pink-900/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="relative">
{/* Survey Title */}
<div className="flex items-start justify-between mb-3">
<h4 className="text-base font-semibold text-gray-900 dark:text-white group-hover:text-purple-700 dark:group-hover:text-purple-300 transition-colors">
{survey.title}
</h4>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-600 dark:text-gray-400">
{survey.totalQuestions} soru
</span>
<span className="text-purple-600 dark:text-purple-400 font-medium">
{survey.responses} yanıt
<div
className={`px-2 py-1 rounded-full text-xs font-medium ${
urgency === 'urgent'
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
: urgency === 'warning'
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
}`}
>
{daysLeft > 0 ? `${daysLeft} gün` : 'Son gün'}
</div>
</div>
{/* Survey Stats */}
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2 text-sm">
<div className="p-1.5 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<FaQuestionCircle className="w-3 h-3 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Sorular</p>
<p className="font-semibold text-gray-900 dark:text-white">
{survey.questions.length}
</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="p-1.5 bg-green-100 dark:bg-green-900/30 rounded-lg">
<FaUsers className="w-3 h-3 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Yanıtlar</p>
<p className="font-semibold text-gray-900 dark:text-white">
{survey.responses}
</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="p-1.5 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<FaClock className="w-3 h-3 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
<p className="font-semibold text-gray-900 dark:text-white">~5dk</p>
</div>
</div>
</div>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex justify-between text-xs mb-1">
<span className="text-gray-600 dark:text-gray-400">Tamamlanma oranı</span>
<span className="text-gray-800 dark:text-gray-200 font-medium">
{Math.round((survey.responses / 100) * 100)}%
</span>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Son tarih: {dayjs(survey.deadline).format('DD MMM')}
<div className="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div
className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${Math.min((survey.responses / 100) * 100, 100)}%` }}
></div>
</div>
</div>
{/* Deadline */}
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
<FaClock className="inline w-3 h-3 mr-1" />
Son tarih: {dayjs(survey.deadline).format('DD MMMM YYYY')}
</p>
<button className="mt-2 w-full px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs rounded-md transition-colors">
{/* Action Button */}
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white text-sm font-medium rounded-lg transition-all duration-300 transform group-hover:scale-[1.02] shadow-sm hover:shadow-md">
Anketi Doldur
<FaArrowRight className="w-3 h-3 transition-transform group-hover:translate-x-1" />
</button>
</div>
))}
{mockSurveys.filter((s) => s.status === 'active').length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
Aktif anket yok
</div>
)
})}
{activeSurveys.length === 0 && (
<div className="text-center py-12">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-full mb-4">
<FaClipboardCheck className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Aktif anket bulunmuyor
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Yeni anketler eklendiğinde burada görünecektir.
</p>
</div>
)}
</div>
</div>