340 lines
12 KiB
TypeScript
340 lines
12 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect, useMemo } from 'react';
|
||
import { completeLesson, type Lesson } from '@/api/schedule';
|
||
import { useAuth } from '@/contexts/AuthContext';
|
||
import { parseISOToUserTimezone } from '@/utils/timezone';
|
||
|
||
interface FeedbackModalProps {
|
||
isOpen: boolean;
|
||
lesson: Lesson | null;
|
||
onClose: () => void;
|
||
onSuccess: () => void;
|
||
}
|
||
|
||
export function FeedbackModal({ isOpen, lesson, onClose, onSuccess }: FeedbackModalProps) {
|
||
const { user } = useAuth();
|
||
const [formData, setFormData] = useState({
|
||
mentor_grade: '',
|
||
school_grade: '',
|
||
mentor_notes: '',
|
||
});
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// Парсим время с учётом timezone пользователя
|
||
const parsedTimes = useMemo(() => {
|
||
if (!lesson) return null;
|
||
return {
|
||
start: parseISOToUserTimezone(lesson.start_time, user?.timezone),
|
||
end: parseISOToUserTimezone(lesson.end_time, user?.timezone),
|
||
};
|
||
}, [lesson, user?.timezone]);
|
||
|
||
useEffect(() => {
|
||
if (isOpen && lesson) {
|
||
setFormData({
|
||
mentor_grade: lesson.mentor_grade?.toString() || '',
|
||
school_grade: lesson.school_grade?.toString() || '',
|
||
mentor_notes: lesson.mentor_notes || '',
|
||
});
|
||
}
|
||
}, [isOpen, lesson]);
|
||
|
||
if (!lesson || !parsedTimes) return null;
|
||
|
||
const visible = isOpen;
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
setError(null);
|
||
setLoading(true);
|
||
try {
|
||
await completeLesson(
|
||
lesson.id,
|
||
formData.mentor_notes.trim(),
|
||
formData.mentor_grade ? parseInt(formData.mentor_grade) : undefined,
|
||
formData.school_grade ? parseInt(formData.school_grade) : undefined
|
||
);
|
||
onSuccess();
|
||
onClose();
|
||
} catch (err: unknown) {
|
||
const errMsg = err instanceof Error ? err.message : 'Ошибка сохранения обратной связи';
|
||
setError(String(errMsg));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleClose = () => {
|
||
if (!loading) {
|
||
setError(null);
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
const clientName =
|
||
typeof lesson.client === 'object' && lesson.client?.user
|
||
? `${lesson.client.user.first_name} ${lesson.client.user.last_name}`
|
||
: (lesson as { client_name?: string }).client_name || 'Студент';
|
||
|
||
const subjectName =
|
||
typeof lesson.subject === 'string'
|
||
? lesson.subject
|
||
: (lesson.subject as { name?: string } | null | undefined)?.name || 'Занятие';
|
||
|
||
return (
|
||
<>
|
||
{/* Затемнённый фон — клик закрывает панель */}
|
||
<div
|
||
role="presentation"
|
||
style={{
|
||
position: 'fixed',
|
||
inset: 0,
|
||
background: 'rgba(0,0,0,0.4)',
|
||
zIndex: 999,
|
||
opacity: visible ? 1 : 0,
|
||
pointerEvents: visible ? 'auto' : 'none',
|
||
transition: 'opacity 0.25s ease',
|
||
}}
|
||
onClick={handleClose}
|
||
/>
|
||
{/* Панель справа */}
|
||
<div
|
||
className="ios26-panel"
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
width: '100%',
|
||
maxWidth: 480,
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '-4px 0 24px rgba(0,0,0,0.15)',
|
||
overflow: 'hidden',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
zIndex: 1000,
|
||
transform: visible ? 'translateX(0)' : 'translateX(100%)',
|
||
transition: 'transform 0.3s ease',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'flex-start',
|
||
justifyContent: 'space-between',
|
||
padding: 24,
|
||
borderBottom: '1px solid var(--md-sys-color-outline-variant)',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<div style={{ minWidth: 0, flex: 1 }}>
|
||
<h2 style={{ fontSize: 20, fontWeight: 600, color: 'var(--md-sys-color-on-surface)', margin: 0 }}>
|
||
Обратная связь
|
||
</h2>
|
||
<p style={{ fontSize: 14, color: 'var(--md-sys-color-on-surface-variant)', marginTop: 4 }}>
|
||
{lesson.title} — {subjectName}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={handleClose}
|
||
disabled={loading}
|
||
style={{
|
||
background: 'none',
|
||
border: 'none',
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
padding: 8,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">close</span>
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} style={{ padding: 24, overflowY: 'auto', flex: 1 }}>
|
||
<div
|
||
style={{
|
||
background: 'var(--md-sys-color-primary-container)',
|
||
borderRadius: 12,
|
||
padding: 16,
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 14 }}>
|
||
<div>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface-variant)' }}>Дата: </span>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface)', fontWeight: 500 }}>
|
||
{parsedTimes.start.dateObj.toLocaleDateString('ru-RU')}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface-variant)' }}>Время: </span>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface)', fontWeight: 500 }}>
|
||
{parsedTimes.start.time}
|
||
{' — '}
|
||
{parsedTimes.end.time}
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface-variant)' }}>Студент: </span>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface)', fontWeight: 500 }}>{clientName}</span>
|
||
</div>
|
||
<div>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface-variant)' }}>Длительность: </span>
|
||
<span style={{ color: 'var(--md-sys-color-on-surface)', fontWeight: 500 }}>
|
||
{(lesson as { duration?: number }).duration || 60} мин
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: 20 }}>
|
||
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12, color: 'var(--md-sys-color-on-surface)' }}>
|
||
Оценки
|
||
</h3>
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
||
<div>
|
||
<label
|
||
htmlFor="mentor_grade"
|
||
style={{ display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}
|
||
>
|
||
Оценка за занятие (1–5)
|
||
</label>
|
||
<input
|
||
id="mentor_grade"
|
||
type="number"
|
||
min={1}
|
||
max={5}
|
||
value={formData.mentor_grade}
|
||
onChange={(e) => setFormData((p) => ({ ...p, mentor_grade: e.target.value }))}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
}}
|
||
placeholder="5"
|
||
disabled={loading}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
htmlFor="school_grade"
|
||
style={{ display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}
|
||
>
|
||
Оценка в школе (1–5)
|
||
</label>
|
||
<input
|
||
id="school_grade"
|
||
type="number"
|
||
min={1}
|
||
max={5}
|
||
value={formData.school_grade}
|
||
onChange={(e) => setFormData((p) => ({ ...p, school_grade: e.target.value }))}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
}}
|
||
placeholder="4"
|
||
disabled={loading}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: 20 }}>
|
||
<label
|
||
htmlFor="mentor_notes"
|
||
style={{ display: 'block', fontSize: 13, fontWeight: 500, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}
|
||
>
|
||
Комментарий к занятию
|
||
</label>
|
||
<textarea
|
||
id="mentor_notes"
|
||
value={formData.mentor_notes}
|
||
onChange={(e) => setFormData((p) => ({ ...p, mentor_notes: e.target.value }))}
|
||
rows={4}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
resize: 'vertical',
|
||
}}
|
||
placeholder="Что прошли на занятии, успехи студента, рекомендации..."
|
||
disabled={loading}
|
||
/>
|
||
</div>
|
||
|
||
{error && (
|
||
<div
|
||
style={{
|
||
background: 'rgba(186,26,26,0.1)',
|
||
border: '1px solid var(--md-sys-color-error)',
|
||
borderRadius: 12,
|
||
padding: 12,
|
||
marginBottom: 20,
|
||
}}
|
||
>
|
||
<p style={{ fontSize: 14, color: 'var(--md-sys-color-error)' }}>{error}</p>
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button
|
||
type="button"
|
||
onClick={handleClose}
|
||
disabled={loading}
|
||
style={{
|
||
flex: 1,
|
||
padding: '14px 24px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'transparent',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
fontSize: 15,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={loading}
|
||
style={{
|
||
flex: 1,
|
||
padding: '14px 24px',
|
||
borderRadius: 14,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 15,
|
||
fontWeight: 600,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
opacity: loading ? 0.7 : 1,
|
||
}}
|
||
>
|
||
{loading ? 'Сохранение...' : 'Сохранить'}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|