Files
mkd/components/pr/NPSSurveyPage.tsx

368 lines
16 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect, useCallback } from 'react';
import { Building2, Star, MessageSquare, CheckCircle2, X, Lock } from 'lucide-react';
import { apiClient, getAuthToken, setAuthToken, fetchGuestToken } from '../../services/apiClient';
import { NPSSurvey } from '../../types';
interface NPSSurveyPageProps {
surveyId: string | number;
apartment?: string; // Номер квартиры из параметров URL
}
// Используем тип из types.ts
export const NPSSurveyPage: React.FC<NPSSurveyPageProps> = ({ surveyId, apartment: apartmentProp }) => {
const [survey, setSurvey] = useState<NPSSurvey | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isAuthorized, setIsAuthorized] = useState(false);
const [accessKeyInput, setAccessKeyInput] = useState('');
const [score, setScore] = useState<number | null>(null);
const [comment, setComment] = useState('');
const [respondentName, setRespondentName] = useState('');
const [apartment, setApartment] = useState(apartmentProp || '');
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
// Получаем номер квартиры из URL параметров, если не передан в пропсах
useEffect(() => {
if (!apartmentProp) {
const urlParams = new URLSearchParams(window.location.search);
const apartmentFromUrl = urlParams.get('apartment');
if (apartmentFromUrl) {
setApartment(apartmentFromUrl);
}
}
}, [apartmentProp]);
const loadSurvey = useCallback(async () => {
if (!surveyId) {
console.error('Survey ID is missing');
setIsLoading(false);
return;
}
try {
setIsLoading(true);
// Гость без учётки: если нет токена — получаем гостевой (доступ к опросам)
if (!getAuthToken()) {
try {
const guestToken = await fetchGuestToken();
setAuthToken(guestToken);
} catch (e) {
console.error('Failed to get guest token:', e);
}
}
console.log(`Loading survey with ID: ${surveyId}`);
const data = await apiClient.get(`/pr/nps-surveys/${surveyId}`);
console.log('Survey loaded:', data);
setSurvey(data);
} catch (err: any) {
console.error('Error loading survey:', err);
const errorMessage = err.response?.data?.error || err.message || 'Неизвестная ошибка';
console.error(`Failed to load survey ${surveyId}:`, errorMessage);
// Не показываем alert сразу - покажем ошибку в UI
setSurvey(null);
} finally {
// Гарантированно устанавливаем isLoading в false
setIsLoading(false);
}
}, [surveyId]);
useEffect(() => {
if (surveyId) {
loadSurvey();
}
}, [surveyId, loadSurvey]);
useEffect(() => {
if (survey) {
// Проверяем ключ из URL
const urlParams = new URLSearchParams(window.location.search);
const keyFromUrl = urlParams.get('key');
if (keyFromUrl === survey.accessKey) {
setIsAuthorized(true);
} else if (!keyFromUrl) {
// Если ключа нет в URL, но опрос загружен - показываем форму ввода
setIsAuthorized(false);
} else {
// Неверный ключ
setIsAuthorized(false);
}
}
}, [survey]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (score === null) {
alert('Пожалуйста, выберите оценку');
return;
}
try {
setIsSubmitting(true);
await apiClient.post(`/pr/nps-surveys/${surveyId}/responses`, {
score,
comment: comment || null,
respondent_name: respondentName || null,
apartment: apartment || null,
access_key: survey?.accessKey
});
setIsSubmitted(true);
} catch (err: any) {
console.error('Error submitting response:', err);
const errorMessage = err.response?.data?.error || err.message || 'Неизвестная ошибка';
alert(`Ошибка отправки ответа: ${errorMessage}`);
} finally {
setIsSubmitting(false);
}
};
// Форма ввода ключа доступа
if (!isAuthorized && survey) {
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-3xl p-8 shadow-2xl border border-slate-200">
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 bg-indigo-100 rounded-full mb-4">
<Lock className="w-8 h-8 text-indigo-600" />
</div>
<h2 className="text-2xl font-black text-slate-800 mb-2">Доступ к опросу</h2>
<p className="text-sm text-slate-600">Введите ключ доступа для участия в опросе</p>
</div>
<div className="space-y-4">
<div>
<label className="block text-xs font-bold text-slate-700 mb-2 uppercase tracking-wider">
Ключ доступа
</label>
<input
type="password"
value={accessKeyInput}
onChange={(e) => setAccessKeyInput(e.target.value)}
placeholder="Введите ключ доступа"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
onKeyPress={(e) => {
if (e.key === 'Enter' && survey && accessKeyInput === survey.accessKey) {
setIsAuthorized(true);
const url = new URL(window.location.href);
url.searchParams.set('key', accessKeyInput);
window.history.pushState({}, '', url.toString());
}
}}
/>
</div>
<button
onClick={() => {
if (survey && accessKeyInput === survey.accessKey) {
setIsAuthorized(true);
const url = new URL(window.location.href);
url.searchParams.set('key', accessKeyInput);
window.history.pushState({}, '', url.toString());
} else {
alert('Неверный ключ доступа');
setAccessKeyInput('');
}
}}
className="w-full px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-bold transition-colors"
>
Войти
</button>
</div>
</div>
</div>
);
}
// Страница благодарности после отправки
if (isSubmitted) {
return (
<div className="min-h-screen bg-gradient-to-br from-emerald-50 via-white to-teal-50 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-3xl p-8 shadow-2xl border border-slate-200 text-center">
<div className="inline-flex items-center justify-center w-20 h-20 bg-emerald-100 rounded-full mb-6">
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
</div>
<h2 className="text-2xl font-black text-slate-800 mb-3">Спасибо за ваш отзыв!</h2>
<p className="text-slate-600 mb-6">
Ваше мнение очень важно для нас. Мы используем ваши ответы для улучшения качества обслуживания.
</p>
<div className="pt-6 border-t border-slate-200">
<p className="text-xs text-slate-400">
Ваш ответ сохранен анонимно
</p>
</div>
</div>
</div>
);
}
// Основная страница опроса
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-indigo-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-slate-600">Загрузка опроса...</p>
</div>
</div>
);
}
// Если опрос не загружен и не загружается - показываем ошибку
if (!survey) {
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 via-white to-pink-50 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-3xl p-8 shadow-2xl border border-red-200 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 bg-red-100 rounded-full mb-4">
<X className="w-8 h-8 text-red-600" />
</div>
<h2 className="text-2xl font-black text-slate-800 mb-3">Ошибка загрузки опроса</h2>
<p className="text-slate-600 mb-6">
Не удалось загрузить опрос. Проверьте правильность ссылки или обратитесь к администратору.
</p>
<p className="text-xs text-slate-400 mb-4">
ID опроса: {surveyId}
</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-bold transition-colors"
>
Обновить страницу
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-indigo-600 to-purple-600 rounded-3xl mb-6 shadow-xl">
<Building2 className="w-10 h-10 text-white" />
</div>
<h1 className="text-4xl font-black text-slate-800 mb-3">{survey.title}</h1>
{survey.description && (
<p className="text-lg text-slate-600 max-w-xl mx-auto">{survey.description}</p>
)}
<div className="mt-4 space-y-2">
{survey.address && (
<p className="text-sm text-slate-500 font-bold">📍 {survey.address}</p>
)}
{apartment && (
<p className="text-base text-indigo-600 font-black bg-indigo-50 px-4 py-2 rounded-xl inline-block">
Квартира {apartment}
</p>
)}
</div>
</div>
{/* Опрос */}
<form onSubmit={handleSubmit} className="bg-white rounded-3xl p-8 shadow-2xl border border-slate-200">
{/* Оценка от 0 до 10 */}
<div className="mb-8">
<label className="block text-lg font-black text-slate-800 mb-6 text-center">
Насколько вероятно, что вы порекомендуете нашу управляющую компанию друзьям или коллегам?
</label>
<div className="flex justify-center gap-2 flex-wrap">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((num) => (
<button
key={num}
type="button"
onClick={() => setScore(num)}
className={`w-14 h-14 rounded-xl font-black text-lg transition-all transform hover:scale-110 ${
score === num
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white shadow-lg scale-110'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{num}
</button>
))}
</div>
<div className="flex justify-between mt-4 text-xs text-slate-500 font-bold">
<span>Точно нет</span>
<span>Нейтрально</span>
<span>Определенно да</span>
</div>
</div>
{/* Дополнительная информация */}
<div className="space-y-4 mb-8">
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Комментарий (необязательно)
</label>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Поделитесь своими мыслями..."
rows={4}
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none"
/>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Ваше имя (необязательно)
</label>
<input
type="text"
value={respondentName}
onChange={(e) => setRespondentName(e.target.value)}
placeholder="Имя"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
{/* Номер квартиры - показываем, если уже указан, или позволяем ввести */}
{apartment ? (
<div className="p-4 bg-indigo-50 rounded-xl border border-indigo-200">
<label className="block text-sm font-bold text-indigo-700 mb-2">
Квартира
</label>
<p className="text-lg font-black text-indigo-800">{apartment}</p>
<button
type="button"
onClick={() => setApartment('')}
className="mt-2 text-xs text-indigo-600 hover:text-indigo-800 font-bold"
>
Изменить
</button>
</div>
) : (
<div>
<label className="block text-sm font-bold text-slate-700 mb-2">
Квартира (необязательно)
</label>
<input
type="text"
value={apartment}
onChange={(e) => setApartment(e.target.value)}
placeholder="№ квартиры"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
)}
</div>
{/* Кнопка отправки */}
<button
type="submit"
disabled={score === null || isSubmitting}
className="w-full px-6 py-4 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white rounded-xl text-base font-black uppercase tracking-wider shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all transform active:scale-95"
>
{isSubmitting ? 'Отправка...' : 'Отправить ответ'}
</button>
<p className="text-xs text-slate-400 text-center mt-4">
Ваши ответы анонимны и используются только для улучшения качества обслуживания
</p>
</form>
</div>
</div>
);
};