Код на отмену урока

Нужен код на отмену урока? Узнайте, как легко освободиться от скучных занятий и вернуть себе свободу прямо сейчас!

В современных онлайн‑школах процесс отмены занятия является одной из самых часто используемых функций. Правильно реализованный механизм позволяет:

  • Сохранить учебный график в актуальном состоянии;
  • Уведомить преподавателя и ученика о смене даты/времени;
  • Сформировать корректный отчет по отменённым и перенесённым урокам;
  • Избежать конфликтов в расписании и двойных записей.

В статье рассмотрим полный стек – от базы данных до клиентского интерфейса, а также лучшие практики и готовый пример кода на C# (ASP.NET Core) и JavaScript.


Бизнес‑требования к функции отмены урока

  1. Кто может отменить? – ученик, преподаватель и администратор.
  2. Время до начала занятия. – если до начала < 24 ч, отмена возможна только администратором.
  3. Причина отмены. – обязательное поле (текстовое или из списка).
  4. Уведомления. – email и push‑уведомление обеим сторонам.
  5. Отчётность. – запись в журнал «Пропущенных и заменённых уроков».
  6. История изменений. – хранение оригинального времени и новой даты (если перенос).

Архитектурный обзор

Схема взаимодействия:

┌─────────────┐ ┌─────────────────────┐
│ Frontend │ HTTP │ API (ASP.NET) │
│ (React/JS) │ ───────► │ /api/lessons/{id} │
└─────┬───────┘ └───────┬─────────────┘
 │ │
 │ 2. Запрос отмены │ 3. Валидация + бизнес‑логика
 ▼ ▼
┌─────────────────────┐ ┌───────────────────────┐
│ База данных (SQL) │ │ Сервис уведомлений │
│ lessons, cancellations│ │ (SMTP, Firebase) │
└─────────────────────┘ └───────────────────────┘

Ключевые таблицы:

  • Lessons – информация о запланированных занятиях.
  • LessonCancellations – записи об отменах/переносах.
  • NotificationQueue – очередь сообщений для отправки.

Реализация на стороне сервера (ASP.NET Core)

Модель данных

public class Lesson
{
 public int Id { get; set; }
 public int TeacherId { get; set; }
 public int StudentId { get; set; }
 public DateTime StartTime { get; set; }
 public DateTime EndTime { get; set; }
 public LessonStatus Status { get; set; } // Scheduled, Cancelled, Rescheduled
 // … другие поля
}

public class LessonCancellation
{
 public int Id { get; set; }
 public int LessonId { get; set; }
 public DateTime CancelledAt { get; set; }
 public string Reason { get; set; }
 public string CancelledBy { get; set; } // "Student", "Teacher", "Admin"
 public DateTime? NewStartTime { get; set; } // если перенос
}

API‑контроллер

[ApiController]
[Route("api/lessons")]
public class LessonsController : ControllerBase
{
 private readonly ApplicationDbContext _db;
 private readonly INotificationService _notifier;

 public LessonsController(ApplicationDbContext db, INotificationService notifier)
 {
 _db = db;
 _notifier = notifier;
 }

 /// <summary>
 /// Отменить или перенести урок.
 /// </summary>
 [HttpPost("{id}/cancel")]
 public async Task CancelLesson(int id, [FromBody] CancelLessonDto dto)
 {
 // 1️⃣ Получаем урок
 var lesson = await _db.Lessons.FindAsync(id);
 if (lesson == null) return NotFound("Урок не найден.");

 // 2️⃣ Проверка прав доступа
 var userId = User.GetUserId; // метод расширения
 var role = User.GetRole; // "Student", "Teacher", "Admin"
 if (!CanCancel(lesson, role, userId, dto.NewStartTime))
 return Forbid("У вас нет прав для отмены данного урока.");

 // 3️⃣ Валидация времени
 var now = DateTime.UtcNow;
 if (lesson.StartTime <= now)
 return BadRequest("Нельзя отменить уже прошедший урок.");

 if (dto.NewStartTime;HasValue && (dto.NewStartTime.Value ⏤ now).TotalHours < 24)
 return BadRequest("Перенос менее чем за 24 часа доступен только администратору.");

 // 4️⃣ Сохраняем запись об отмене
 var cancellation = new LessonCancellation
 {
 LessonId = lesson.Id,
 CancelledAt = now,
 Reason = dto.Reason,
 CancelledBy = role,
 NewStartTime = dto.NewStartTime

 };
 _db.LessonCancellations.Add(cancellation);

 // 5️⃣ Обновляем статус урока
 lesson.Status = dto.NewStartTime.HasValue ? LessonStatus.Rescheduled : LessonStatus.Cancelled;
 if (dto.NewStartTime.HasValue)
 lesson;StartTime = dto.NewStartTime.Value; // перенос

 await _db.SaveChangesAsync;

 // 6️⃣ Уведомляем стороны
 await _notifier.SendLessonCancellationAsync(lesson, cancellation);

 return Ok(new { message = "Урок успешно отменён/перенесён." });
 }

 private bool CanCancel(Lesson lesson, string role, int userId, DateTime? newStart)
 {
 // Студент может отменять только свои занятия
 if (role == "Student" && lesson.StudentId != userId) return false;
 // Преподаватель – только свои
 if (role == "Teacher" && lesson.TeacherId != userId) return false;
 // Администратор – без ограничений
 return true;
 }
}

public class CancelLessonDto
{
 public string Reason { get; set; } // обязательное поле
 public DateTime? NewStartTime { get; set; } // null – полная отмена
}

Сервис уведомлений

public interface INotificationService
{
 Task SendLessonCancellationAsync(Lesson lesson, LessonCancellation cancellation);
}

public class EmailNotificationService : INotificationService
{
 private readonly SmtpClient _smtp;
 public EmailNotificationService(SmtpClient smtp) => _smtp = smtp;

 public async Task SendLessonCancellationAsync(Lesson lesson, LessonCancellation cancellation)
 {
 var studentMail = await GetUserEmailAsync(lesson.StudentId);
 var teacherMail = await GetUserEmailAsync(lesson.TeacherId);

 var subject = cancellation.NewStartTime.HasValue
 ? "Урок перенесён"
 : "Урок отменён";

 var body = $@"
 Здравствуйте,

 Урок, запланированный на {lesson;StartTime:dd.MM.yyyy HH:mm},
 был {(cancellation.NewStartTime.HasValue ? "перенесён" : "отменён")}.

 Причина: {cancellation.Reason}
 {(cancellation.NewStartTime.HasValue ? $"Новая дата: {cancellation.NewStartTime:dd.MM.yyyy HH:mm}" : "")}

 С уважением,
 Онлайн‑школа
 ";

 var msg = new MailMessage("no-reply@school.com", studentMail, subject, body);
 await _smtp.SendMailAsync(msg);

 // отправляем учителю
 msg = new MailMessage("no-reply@school.com", teacherMail, subject, body);
 await _smtp.SendMailAsync(msg);
 }

 private Task GetUserEmailAsync(int userId)
 {
 // запрос к базе или к Identity‑сервису
 // упрощённо:
 return Task.FromResult($"user{userId}@example.com");
 }
}

Реализация на клиенте (JavaScript/React)

UI‑компонент

import React, { useState } from 'react';
import axios from 'axios';

function CancelLessonButton({ lessonId, startTime }) {
 const [showForm, setShowForm] = useState(false);
 const [reason, setReason] = useState('');
 const [newDate, setNewDate] = useState('');
 const [error, setError] = useState(null);
 const [loading, setLoading] = useState(false);

 const handleCancel = async  => {
 setLoading(true);
 setError(null);
 try {
 const payload = {
 reason,
 newStartTime: newDate ? new Date(newDate).toISOString : null,
 };
 const resp = await axios.post(
 `/api/lessons/${lessonId}/cancel`,
 payload
 );
 alert(resp.data.message);
 // Обновить UI – например, вызвать refetch списка уроков
 } catch (e) {
 setError(e.response?.data || 'Произошла ошибка');
 } finally {
 setLoading(false);
 }
 };
 return (
 <div className="cancel-lesson">
 <button onClick={ => setShowForm(!showForm)}>
 {showForm ? 'Скрыть форму' : 'Отменить/перенести урок'}
 </button>

 {showForm && (
 <div className="form">
 <label>Причина (обязательно):</label>
 <textarea
 value={reason}
 onChange={e => setReason(e.target.value)}
 required
 />

 <label>Новая дата/время (если перенос):</label>
 <input
 type="datetime-local"
 value={newDate}
 onChange={e => setNewDate(e.target.value)}
 />

 {error && <p className="error">{error}</p>}

 <button onClick={handleCancel} disabled={loading || !reason.trim}>
 {loading ? 'Отправка…' : 'Подтвердить'}
 </button>
 </div>
 )}
 </div>
 );
}export default CancelLessonButton;

Обработка ответа и обновление UI

После успешного запроса рекомендуется:

  • Обновить локальный кэш расписания (например, через react-query).
  • Показать пользователю toast‑сообщение о результате.
  • Если был перенос, отобразить новое время в списке уроков.

Уведомления и отчётность

Журнал отмен

CREATE TABLE LessonCancellations (
 Id INT PRIMARY KEY IDENTITY,
 LessonId INT NOT NULL,
 CancelledAt DATETIME2 NOT NULL,
 Reason NVARCHAR(500) NOT NULL,
 CancelledBy NVARCHAR(20) NOT NULL,
 NewStartTime DATETIME2 NULL,
 CONSTRAINT FK_LessonCancellations_Lessons
 FOREIGN KEY (LessonId) REFERENCES Lessons(Id)
);

Формирование отчёта

Пример запроса для администраторов:

SELECT
 l.Id AS LessonId,
 l.StartTime AS OriginalStart,
 c.CancelledAt,
 c.Reason,
 c.CancelledBy,
 c.NewStartTime
FROM Lessons l
JOIN LessonCancellations c ON l.Id = c.LessonId
WHERE c.CancelledAt BETWEEN @FromDate AND @ToDate
ORDER BY c.CancelledAt DESC;

Триггер для автоматической записи в журнал

CREATE TRIGGER trg_AfterLessonUpdate
ON Lessons
AFTER UPDATE
AS
BEGIN
 SET NOCOUNT ON;
 IF UPDATE(Status)
 BEGIN
 INSERT INTO LessonCancellations (LessonId, CancelledAt, Reason, CancelledBy, NewStartTime)
 SELECT
 inserted.Id,
 GETUTCDATE,
 'Системный перенос',
 'System',
 CASE WHEN inserted;Status = 'Rescheduled' THEN inserted.StartTime END
 FROM inserted
 WHERE inserted.Status IN ('Cancelled','Rescheduled');
 END
END;

Особые случаи и рекомендации

Отмена менее чем за 24 ч

  • Только администратор имеет право отменять/переносить в этот период.
  • В UI скрыть кнопку «Отменить» для студентов/преподавателей, если до начала < 24 ч.

Повторные отмены

Если урок уже отмечен как Cancelled, повторный запрос должен вернуть 400 Bad Request с сообщением «Урок уже отменён».

Защита от подделки запросов (CSRF)

Для SPA‑приложений используйте токен SameSite=Lax + заголовок X‑XSRF‑Token. На сервере проверяйте его в каждом POST‑запросе.

Логирование

Каждое действие отмены следует записывать в централизованный лог (Serilog, ELK) с полями:

  • userId, role
  • lessonId
  • action = "Cancel" / "Reschedule"
  • timestamp
  • reason

Тестирование

  1. Unit‑тесты контроллера (мок репозитория, проверка прав).
  2. Integration‑тесты с реальной БД (EF Core InMemory).
  3. E2E‑тесты (Cypress) – имитировать отмену урока пользователем разных ролей.

Полный пример проекта

Для быстрого старта можно клонировать репозиторий:

git clone https://github.com/example/online-school-cancel-demo.git
cd online-school-cancel-demo
dotnet build
dotnet run

В проекте уже присутствуют:

  • Entity Framework Core модели Lesson и LessonCancellation;
  • API‑контроллер LessonsController с методом CancelLesson;
  • React‑компонент CancelLessonButton;
  • Сервис отправки email через SmtpClient;
  • Тесты на xUnit и Cypress.

Функция отмены урока – ключевой элемент любой онлайн‑школы. При правильной реализации она обеспечивает:

  • Прозрачность расписания;
  • Своевременное информирование всех участников;
  • Надёжный аудит и возможность анализа причин отмен.

Следуя рекомендациям из этой статьи, вы сможете быстро внедрить безопасный и удобный механизм отмены/переноса занятий, а также адаптировать его под любые бизнес‑правила вашей школы.

Помните: всегда проверяйте права пользователя, валидируйте время и сохраняйте полную историю изменений – это поможет избежать конфликтов и упростит работу администраторов.


Дата публикации: