sozsoft-platform/ui/src/views/intranet/widgets/SurveyModal.tsx
2026-05-08 11:38:57 +03:00

313 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { 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