884 lines
33 KiB
TypeScript
884 lines
33 KiB
TypeScript
/**
|
||
* Блок «Список занятий на выбранный день» / «Форма создания/редактирования» с анимацией cube.
|
||
* Используется в Dashboard и других страницах.
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import React, { useState } from 'react';
|
||
import { format, startOfDay, addDays, subDays } from 'date-fns';
|
||
import { ru } from 'date-fns/locale';
|
||
import { LessonCard } from '@/components/dashboard/LessonCard';
|
||
import { StudentSelect } from '@/components/dashboard/StudentSelect';
|
||
import { SubjectSelect } from '@/components/dashboard/SubjectSelect';
|
||
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
|
||
import { Switch } from '@/components/common/Switch';
|
||
import { DatePicker } from '@/components/common/DatePicker';
|
||
import { TimePicker } from '@/components/common/TimePicker';
|
||
import type { LessonPreview } from '@/api/dashboard';
|
||
import type { Student } from '@/api/students';
|
||
import type { Subject, MentorSubject } from '@/api/subjects';
|
||
|
||
export interface CheckLessonFormData {
|
||
client: string;
|
||
title: string;
|
||
description: string;
|
||
start_date: string;
|
||
start_time: string;
|
||
duration: number;
|
||
price: number | undefined;
|
||
is_recurring: boolean;
|
||
}
|
||
|
||
export interface CheckLessonProps {
|
||
/** Выбранная дата */
|
||
selectedDate: Date;
|
||
/** Дата для отображения в заголовке (например, displayDate) */
|
||
displayDate: Date;
|
||
/** Идёт загрузка занятий */
|
||
lessonsLoading: boolean;
|
||
/** Занятия на выбранный день */
|
||
lessonsForSelectedDate: LessonPreview[];
|
||
/** Открыта ли форма (создание/редактирование) — переворот cube */
|
||
isFormVisible: boolean;
|
||
/** Предыдущий день */
|
||
onPrevDay: () => void;
|
||
/** Следующий день */
|
||
onNextDay: () => void;
|
||
/** Добавить занятие (открыть форму создания) */
|
||
onAddLesson: () => void;
|
||
/** Клик по карточке занятия (открыть форму редактирования или просмотра) */
|
||
onLessonClick: (lesson: { id: string }) => void;
|
||
/** Ментор — может добавлять и редактировать; для остальных только просмотр */
|
||
isMentor?: boolean;
|
||
/** Кнопка «Добавить» и компоненты загружены */
|
||
buttonComponentsLoaded?: boolean;
|
||
|
||
// Форма
|
||
formComponentsLoaded: boolean;
|
||
lessonEditLoading: boolean;
|
||
isEditingMode: boolean;
|
||
formLoading: boolean;
|
||
formError: string | null;
|
||
formData: CheckLessonFormData;
|
||
setFormData: React.Dispatch<React.SetStateAction<CheckLessonFormData>>;
|
||
selectedSubjectId: number | null;
|
||
selectedMentorSubjectId: number | null;
|
||
onSubjectChange: (subjectId: number | null, mentorSubjectId: number | null) => void;
|
||
students: Student[];
|
||
subjects: Subject[];
|
||
mentorSubjects: MentorSubject[];
|
||
onSubmit: (e: React.FormEvent) => void;
|
||
onCancel: () => void;
|
||
/** Удалить занятие (только в режиме редактирования). deleteAllFuture — удалить всю цепочку постоянных. */
|
||
onDelete?: (deleteAllFuture: boolean) => void;
|
||
}
|
||
|
||
export const CheckLesson: React.FC<CheckLessonProps> = ({
|
||
selectedDate,
|
||
displayDate,
|
||
lessonsLoading,
|
||
lessonsForSelectedDate,
|
||
isFormVisible,
|
||
onPrevDay,
|
||
onNextDay,
|
||
onAddLesson,
|
||
onLessonClick,
|
||
isMentor = false,
|
||
buttonComponentsLoaded = false,
|
||
formComponentsLoaded,
|
||
lessonEditLoading,
|
||
isEditingMode,
|
||
formLoading,
|
||
formError,
|
||
formData,
|
||
setFormData,
|
||
selectedSubjectId,
|
||
selectedMentorSubjectId,
|
||
onSubjectChange,
|
||
students,
|
||
subjects,
|
||
mentorSubjects,
|
||
onSubmit,
|
||
onCancel,
|
||
onDelete,
|
||
}) => {
|
||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||
const navDisabled = lessonsLoading || isFormVisible;
|
||
const formDisabled = formLoading || lessonEditLoading;
|
||
|
||
const headerDateLabel = React.useMemo(() => {
|
||
const now = new Date();
|
||
const sameYear = displayDate?.getFullYear?.() === now.getFullYear();
|
||
const fmt = sameYear ? 'd MMMM' : 'd MMMM yyyy';
|
||
try {
|
||
return format(displayDate, fmt, { locale: ru });
|
||
} catch {
|
||
return '';
|
||
}
|
||
}, [displayDate]);
|
||
|
||
const handleDeleteClick = () => {
|
||
if (!onDelete) return;
|
||
if (formData.is_recurring) {
|
||
setShowDeleteConfirm(true);
|
||
} else {
|
||
if (typeof window !== 'undefined' && window.confirm('Удалить занятие?')) {
|
||
onDelete(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleDeleteConfirm = (deleteAllFuture: boolean) => {
|
||
onDelete?.(deleteAllFuture);
|
||
setShowDeleteConfirm(false);
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className="checklesson-root"
|
||
style={{
|
||
position: 'relative',
|
||
width: '100%',
|
||
height: '100%',
|
||
minHeight: '548px',
|
||
perspective: '1000px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: 'relative',
|
||
width: '100%',
|
||
height: '100%',
|
||
transformStyle: 'preserve-3d',
|
||
transition: 'transform 0.6s ease-in-out',
|
||
transform: isFormVisible ? 'rotateY(180deg)' : 'rotateY(0deg)',
|
||
}}
|
||
>
|
||
{/* Лицевая сторона: Список занятий */}
|
||
<div
|
||
className="ios-glass-panel"
|
||
style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
width: '100%',
|
||
height: '100%',
|
||
backfaceVisibility: 'hidden',
|
||
WebkitBackfaceVisibility: 'hidden',
|
||
transform: 'rotateY(0deg)',
|
||
borderRadius: '20px',
|
||
padding: '24px',
|
||
overflowY: 'hidden',
|
||
transformOrigin: 'center center',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '20px' }}>
|
||
<div
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: '40px 1fr 40px',
|
||
alignItems: 'center',
|
||
columnGap: 12,
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={onPrevDay}
|
||
disabled={navDisabled}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
width: 40,
|
||
height: 40,
|
||
padding: 0,
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
border: 'none',
|
||
borderRadius: 20,
|
||
cursor: navDisabled ? 'not-allowed' : 'pointer',
|
||
opacity: navDisabled ? 0.6 : 1,
|
||
transition: 'all 0.2s ease',
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!navDisabled) {
|
||
e.currentTarget.style.opacity = '0.8';
|
||
e.currentTarget.style.transform = 'scale(0.95)';
|
||
}
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.opacity = navDisabled ? '0.6' : '1';
|
||
e.currentTarget.style.transform = 'scale(1)';
|
||
}}
|
||
title="Предыдущий день"
|
||
>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<polyline points="15 18 9 12 15 6" />
|
||
</svg>
|
||
</button>
|
||
|
||
<h3
|
||
style={{
|
||
fontSize: '20px',
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
margin: 0,
|
||
textAlign: 'center',
|
||
}}
|
||
>
|
||
{headerDateLabel}
|
||
</h3>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={onNextDay}
|
||
disabled={navDisabled}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
width: 40,
|
||
height: 40,
|
||
padding: 0,
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
border: 'none',
|
||
borderRadius: 20,
|
||
cursor: navDisabled ? 'not-allowed' : 'pointer',
|
||
opacity: navDisabled ? 0.6 : 1,
|
||
transition: 'all 0.2s ease',
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!navDisabled) {
|
||
e.currentTarget.style.opacity = '0.8';
|
||
e.currentTarget.style.transform = 'scale(0.95)';
|
||
}
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.opacity = navDisabled ? '0.6' : '1';
|
||
e.currentTarget.style.transform = 'scale(1)';
|
||
}}
|
||
title="Следующий день"
|
||
>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<polyline points="9 18 15 12 9 6" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{/* Контент (скроллится) */}
|
||
<div style={{ flex: 1, overflowY: 'auto', paddingRight: 2 }}>
|
||
{lessonsLoading ? (
|
||
<LoadingSpinner size="medium" />
|
||
) : lessonsForSelectedDate.length === 0 ? (
|
||
<div
|
||
style={{
|
||
textAlign: 'center',
|
||
padding: '40px 20px',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
}}
|
||
>
|
||
<svg
|
||
width="48"
|
||
height="48"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
style={{ margin: '0 auto 16px', opacity: 0.5 }}
|
||
>
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||
<line x1="16" y1="2" x2="16" y2="6" />
|
||
<line x1="8" y1="2" x2="8" y2="6" />
|
||
<line x1="3" y1="10" x2="21" y2="10" />
|
||
</svg>
|
||
<p style={{ margin: 0, fontSize: '14px' }}>Нет занятий на этот день</p>
|
||
</div>
|
||
) : (
|
||
<div>
|
||
{lessonsForSelectedDate.map((lesson) => (
|
||
<LessonCard
|
||
key={lesson.id}
|
||
lesson={lesson}
|
||
showClient
|
||
onClick={isMentor ? () => onLessonClick(lesson) : undefined}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer — кнопка «Добавить занятие» только для ментора */}
|
||
{buttonComponentsLoaded && isMentor && (
|
||
<div
|
||
style={{
|
||
paddingTop: 16,
|
||
marginTop: 12,
|
||
borderTop: '1px solid var(--md-sys-color-outline-variant)',
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={onAddLesson}
|
||
disabled={navDisabled}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '10px 24px',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
border: 'none',
|
||
borderRadius: 20,
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
cursor: navDisabled ? 'not-allowed' : 'pointer',
|
||
opacity: navDisabled ? 0.6 : 1,
|
||
transition: 'all 0.2s ease',
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
if (!navDisabled) e.currentTarget.style.opacity = '0.9';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.opacity = navDisabled ? '0.6' : '1';
|
||
}}
|
||
>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<line x1="12" y1="5" x2="12" y2="19" />
|
||
<line x1="5" y1="12" x2="19" y2="12" />
|
||
</svg>
|
||
Добавить занятие
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Обратная сторона: Форма создания/редактирования */}
|
||
<div
|
||
className="ios-glass-panel"
|
||
style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
width: '100%',
|
||
height: '100%',
|
||
backfaceVisibility: 'hidden',
|
||
WebkitBackfaceVisibility: 'hidden',
|
||
transform: 'rotateY(180deg)',
|
||
borderRadius: '20px',
|
||
padding: '24px',
|
||
overflowY: 'auto',
|
||
transformOrigin: 'center center',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
}}
|
||
>
|
||
{!formComponentsLoaded || (isEditingMode && lessonEditLoading) ? (
|
||
<LoadingSpinner size="medium" />
|
||
) : (
|
||
<>
|
||
<form
|
||
onSubmit={onSubmit}
|
||
className="checklesson-form"
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: '1fr 1fr',
|
||
gap: 16,
|
||
position: 'relative',
|
||
zIndex: 10,
|
||
pointerEvents: 'auto',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
gridColumn: '1 / -1',
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: 8,
|
||
}}
|
||
>
|
||
<h3
|
||
style={{
|
||
fontSize: '20px',
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
margin: 0,
|
||
}}
|
||
>
|
||
{isEditingMode ? 'Редактировать занятие' : 'Создать занятие'}
|
||
</h3>
|
||
<button
|
||
type="button"
|
||
onClick={onCancel}
|
||
disabled={formDisabled}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
width: 40,
|
||
height: 40,
|
||
padding: 0,
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
border: 'none',
|
||
borderRadius: 20,
|
||
cursor: formDisabled ? 'not-allowed' : 'pointer',
|
||
opacity: formDisabled ? 0.6 : 1,
|
||
transition: 'all 0.2s ease',
|
||
}}
|
||
title="Отмена"
|
||
>
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<line x1="18" y1="6" x2="6" y2="18" />
|
||
<line x1="6" y1="6" x2="18" y2="18" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{formError && (
|
||
<div
|
||
style={{
|
||
gridColumn: '1 / -1',
|
||
padding: 12,
|
||
borderRadius: 8,
|
||
background: 'var(--md-sys-color-error-container)',
|
||
color: 'var(--md-sys-color-on-error-container)',
|
||
fontSize: 14,
|
||
whiteSpace: 'pre-wrap' as const,
|
||
}}
|
||
>
|
||
{formError}
|
||
</div>
|
||
)}
|
||
|
||
<div style={{ gridColumn: 1 }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Ученик *
|
||
</label>
|
||
<StudentSelect
|
||
students={students}
|
||
value={formData.client}
|
||
onChange={(value) => setFormData((prev) => ({ ...prev, client: value }))}
|
||
disabled={formLoading || isEditingMode}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: 2 }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Предмет *
|
||
</label>
|
||
<SubjectSelect
|
||
subjects={subjects}
|
||
mentorSubjects={mentorSubjects}
|
||
value={selectedSubjectId ?? selectedMentorSubjectId ?? null}
|
||
onChange={(value) => {
|
||
if (value != null) {
|
||
const s = subjects.find((x) => x.id === value);
|
||
if (s) onSubjectChange(value, null);
|
||
else onSubjectChange(null, value);
|
||
} else {
|
||
onSubjectChange(null, null);
|
||
}
|
||
}}
|
||
disabled={formLoading}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: '1 / -1' }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Описание
|
||
</label>
|
||
<textarea
|
||
value={formData.description}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||
placeholder="Дополнительная информация о занятии"
|
||
disabled={formLoading}
|
||
rows={2}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
fontSize: 16,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
borderRadius: 4,
|
||
fontFamily: 'inherit',
|
||
resize: 'vertical',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s ease',
|
||
}}
|
||
onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--md-sys-color-primary)';
|
||
e.currentTarget.style.borderWidth = '2px';
|
||
}}
|
||
onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--md-sys-color-outline)';
|
||
e.currentTarget.style.borderWidth = '1px';
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: 1 }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Дата начала *
|
||
</label>
|
||
<DatePicker
|
||
value={formData.start_date}
|
||
onChange={(value) => setFormData((prev) => ({ ...prev, start_date: value }))}
|
||
disabled={formLoading}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: 2 }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Время начала *
|
||
</label>
|
||
<TimePicker
|
||
value={formData.start_time}
|
||
onChange={(value) => setFormData((prev) => ({ ...prev, start_time: value }))}
|
||
disabled={formLoading}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: 1 }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Длительность (минуты) *
|
||
</label>
|
||
<select
|
||
value={formData.duration}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, duration: Number(e.target.value) }))}
|
||
required
|
||
disabled={formLoading}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
fontSize: 16,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
borderRadius: 4,
|
||
fontFamily: 'inherit',
|
||
cursor: formLoading ? 'not-allowed' : 'pointer',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s ease',
|
||
}}
|
||
onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--md-sys-color-primary)';
|
||
e.currentTarget.style.borderWidth = '2px';
|
||
}}
|
||
onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--md-sys-color-outline)';
|
||
e.currentTarget.style.borderWidth = '1px';
|
||
}}
|
||
>
|
||
<option value={30}>30 минут</option>
|
||
<option value={45}>45 минут</option>
|
||
<option value={60}>1 час</option>
|
||
<option value={90}>1.5 часа</option>
|
||
<option value={120}>2 часа</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: 2 }}>
|
||
<label
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
marginBottom: 4,
|
||
}}
|
||
>
|
||
Стоимость (₽) *
|
||
</label>
|
||
<input
|
||
type="number"
|
||
value={formData.price ?? ''}
|
||
onChange={(e) => setFormData((prev) => ({ ...prev, price: Number(e.target.value) }))}
|
||
required
|
||
min={0}
|
||
step={100}
|
||
placeholder="1000"
|
||
disabled={formLoading}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
fontSize: 16,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
borderRadius: 4,
|
||
fontFamily: 'inherit',
|
||
outline: 'none',
|
||
transition: 'border-color 0.2s ease',
|
||
}}
|
||
onFocus={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--md-sys-color-primary)';
|
||
e.currentTarget.style.borderWidth = '2px';
|
||
}}
|
||
onBlur={(e) => {
|
||
e.currentTarget.style.borderColor = 'var(--md-sys-color-outline)';
|
||
e.currentTarget.style.borderWidth = '1px';
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ gridColumn: '1 / -1', padding: '8px 0' }}>
|
||
<Switch
|
||
checked={formData.is_recurring}
|
||
onChange={(checked) => setFormData((prev) => ({ ...prev, is_recurring: checked }))}
|
||
disabled={formLoading}
|
||
label="Постоянное занятие (повторяется еженедельно)"
|
||
/>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
gridColumn: '1 / -1',
|
||
display: 'flex',
|
||
gap: 12,
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginTop: 8,
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
{isEditingMode && onDelete && (
|
||
<button
|
||
type="button"
|
||
onClick={handleDeleteClick}
|
||
disabled={formDisabled}
|
||
style={{
|
||
padding: '10px 24px',
|
||
background: 'transparent',
|
||
color: 'var(--md-sys-color-error)',
|
||
border: '1px solid var(--md-sys-color-error)',
|
||
borderRadius: 20,
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
cursor: formDisabled ? 'not-allowed' : 'pointer',
|
||
opacity: formDisabled ? 0.6 : 1,
|
||
}}
|
||
>
|
||
Удалить
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button
|
||
type="button"
|
||
onClick={onCancel}
|
||
disabled={formDisabled}
|
||
style={{
|
||
padding: '10px 24px',
|
||
background: 'transparent',
|
||
color: 'var(--md-sys-color-primary)',
|
||
border: '1px solid var(--md-sys-color-outline-variant)',
|
||
borderRadius: 20,
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
cursor: formDisabled ? 'not-allowed' : 'pointer',
|
||
opacity: formDisabled ? 0.6 : 1,
|
||
}}
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
disabled={formLoading}
|
||
style={{
|
||
padding: '10px 24px',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
border: 'none',
|
||
borderRadius: 20,
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
cursor: formLoading ? 'not-allowed' : 'pointer',
|
||
opacity: formLoading ? 0.6 : 1,
|
||
}}
|
||
>
|
||
{formLoading
|
||
? isEditingMode
|
||
? 'Сохранение...'
|
||
: 'Создание...'
|
||
: isEditingMode
|
||
? 'Сохранить'
|
||
: 'Создать'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
{showDeleteConfirm && formData.is_recurring && (
|
||
<div
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="delete-dialog-title"
|
||
style={{
|
||
position: 'fixed',
|
||
inset: 0,
|
||
zIndex: 1000,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(0,0,0,0.4)',
|
||
}}
|
||
onClick={() => setShowDeleteConfirm(false)}
|
||
>
|
||
<div
|
||
style={{
|
||
background: 'var(--md-sys-color-surface)',
|
||
borderRadius: 16,
|
||
padding: 24,
|
||
maxWidth: 400,
|
||
width: '90%',
|
||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<h4
|
||
id="delete-dialog-title"
|
||
style={{
|
||
margin: '0 0 12px',
|
||
fontSize: 18,
|
||
fontWeight: 500,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
}}
|
||
>
|
||
Удалить занятие
|
||
</h4>
|
||
<p
|
||
style={{
|
||
margin: '0 0 20px',
|
||
fontSize: 14,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
lineHeight: 1.4,
|
||
}}
|
||
>
|
||
Это постоянное занятие. Удалить только это занятие или всю цепочку будущих?
|
||
</p>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleDeleteConfirm(false)}
|
||
disabled={formLoading}
|
||
style={{
|
||
padding: '10px 16px',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
border: 'none',
|
||
borderRadius: 12,
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
cursor: formLoading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Только это занятие
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleDeleteConfirm(true)}
|
||
disabled={formLoading}
|
||
style={{
|
||
padding: '10px 16px',
|
||
background: 'var(--md-sys-color-error)',
|
||
color: 'var(--md-sys-color-on-error)',
|
||
border: 'none',
|
||
borderRadius: 12,
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
cursor: formLoading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Всю цепочку занятий
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowDeleteConfirm(false)}
|
||
disabled={formLoading}
|
||
style={{
|
||
padding: '10px 16px',
|
||
background: 'transparent',
|
||
color: 'var(--md-sys-color-primary)',
|
||
border: '1px solid var(--md-sys-color-outline-variant)',
|
||
borderRadius: 12,
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
cursor: formLoading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|