313 lines
12 KiB
TypeScript
313 lines
12 KiB
TypeScript
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()
|
||
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 {}
|
||
})
|
||
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) {
|
||
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')
|
||
}
|
||
}
|
||
})
|
||
|
||
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>
|
||
<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>
|
||
{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.text}
|
||
checked={answers[question.id] === option.text}
|
||
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">
|
||
ℹ️ {translate('::App.Platform.Intranet.SurveyModal.RequiredUserName')}
|
||
</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">
|
||
✅ {translate('::App.Platform.Intranet.SurveyModal.AnonymousNotice')}
|
||
</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"
|
||
>
|
||
{isUpdate
|
||
? translate('::App.Platform.Intranet.SurveyModal.Update')
|
||
: translate('::App.Platform.Intranet.SurveyModal.Submit')}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default SurveyModal
|