uchill/front_material/components/schedule/FeedbackModal.tsx

340 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

'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 }}
>
Оценка за занятие (15)
</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 }}
>
Оценка в школе (15)
</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>
</>
);
}