2026-05-05 17:59:30 +00:00
|
|
|
|
import React, { useState } from 'react'
|
|
|
|
|
|
import { motion } from 'framer-motion'
|
|
|
|
|
|
import { FaTimes } from 'react-icons/fa'
|
|
|
|
|
|
import { SurveyAnswerDto, SurveyDto, SurveyQuestionDto } from '@/proxy/intranet/models'
|
|
|
|
|
|
|
|
|
|
|
|
interface SurveyModalProps {
|
|
|
|
|
|
survey: SurveyDto
|
|
|
|
|
|
onClose: () => void
|
|
|
|
|
|
onSubmit: (answers: SurveyAnswerDto[]) => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit }) => {
|
|
|
|
|
|
const { translate } = useLocalization();
|
2026-05-06 07:54:04 +00:00
|
|
|
|
const isUpdate = !!survey.myResponse
|
|
|
|
|
|
|
|
|
|
|
|
const [answers, setAnswers] = useState<{ [questionId: string]: any }>(() => {
|
|
|
|
|
|
if (survey.myResponse?.answers) {
|
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
|
survey.myResponse.answers.map((a) => [
|
|
|
|
|
|
a.questionId,
|
|
|
|
|
|
a.questionType === 'rating' ? Number(a.value) : a.value,
|
|
|
|
|
|
])
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
return {}
|
|
|
|
|
|
})
|
2026-05-05 17:59:30 +00:00
|
|
|
|
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) => {
|
2026-05-06 07:54:04 +00:00
|
|
|
|
if (question.isRequired) {
|
|
|
|
|
|
const val = answers[question.id]
|
|
|
|
|
|
const isEmpty = val === undefined || val === null || val === '' || (question.type === 'rating' && Number(val) === 0)
|
|
|
|
|
|
if (isEmpty) {
|
|
|
|
|
|
newErrors[question.id] = translate('::App.Platform.Intranet.SurveyModal.RequiredField')
|
|
|
|
|
|
}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors)
|
|
|
|
|
|
return Object.keys(newErrors).length === 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
|
|
|
|
if (!validateAnswers()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const surveyAnswers: SurveyAnswerDto[] = 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: SurveyQuestionDto, 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>
|
2026-05-06 07:54:04 +00:00
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={star}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => handleAnswerChange(question.id, star)}
|
|
|
|
|
|
className={`text-2xl transition-colors ${
|
|
|
|
|
|
(answers[question.id] ?? 0) >= star
|
|
|
|
|
|
? 'text-yellow-400'
|
|
|
|
|
|
: 'text-gray-300 dark:text-gray-600 hover:text-yellow-300'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
★
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-05-05 17:59:30 +00:00
|
|
|
|
{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}`}
|
2026-05-06 07:54:04 +00:00
|
|
|
|
value={option.text}
|
|
|
|
|
|
checked={answers[question.id] === option.text}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
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={translate('::App.Platform.Intranet.SurveyModal.AnswerPlaceholder')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{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={translate('::App.Platform.Intranet.SurveyModal.CommentPlaceholder')}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{hasError && (
|
|
|
|
|
|
<p className="text-sm text-red-600 dark:text-red-400">{errors[question.id]}</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
|
exit={{ opacity: 0 }}
|
|
|
|
|
|
className="fixed inset-0 bg-black/50 z-40"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
|
|
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
|
|
|
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800 z-10">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
|
|
|
|
{survey.title}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{survey.description}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTimes className="w-5 h-5 text-gray-500" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<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 && (
|
|
|
|
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
|
|
|
|
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
|
|
|
|
|
ℹ️ Bu anket isim belirtilerek doldurulmaktadır. Yanıtlarınız kaydedilecektir.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{survey.isAnonymous && (
|
|
|
|
|
|
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-3">
|
|
|
|
|
|
<p className="text-sm text-green-700 dark:text-green-300">
|
|
|
|
|
|
✅ Bu anket anonimdir. Kimlik bilgileriniz kaydedilmeyecektir.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
{translate('::Cancel')}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="submit"
|
|
|
|
|
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
|
|
|
|
>
|
2026-05-06 07:54:04 +00:00
|
|
|
|
{isUpdate
|
|
|
|
|
|
? translate('::App.Platform.Intranet.SurveyModal.Update')
|
|
|
|
|
|
: translate('::App.Platform.Intranet.SurveyModal.Submit')}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default SurveyModal
|