sozsoft-platform/ui/src/views/intranet/widgets/SurveyModal.tsx

310 lines
12 KiB
TypeScript
Raw Normal View History

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 {}
})
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')
}
}
})
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>
{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}
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')}
</button>
</div>
</form>
</motion.div>
</div>
</>
)
}
export default SurveyModal