<?php
namespace App\Controller;
use App\Entity\CalendarEvent;
use App\Entity\Candidate;
use App\Entity\Person;
use App\Entity\Student;
use App\Entity\StudentLevel;
use App\Enum\Student\Status;
use App\Enum\Student\VirtualStatus;
use App\Form\PersonType;
use App\Form\StudentType;
use App\Library\PyTg\PyTgUtils;
use App\Library\TdLib\TdLibObject\Model\Message\Message;
use App\Library\Utils\Other\Other;
use App\Service\CalendarEvent\CalendarEventService;
use App\Service\Candidate\CandidateService;
use App\Service\Job\JobService;
use App\Service\Person\PersonService;
use App\Service\Student\StudentService;
use App\Service\StudentLevel\StudentLevelService;
use App\Service\User\UserService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class StudentController extends BaseAbstractController
{
public function list(Request $request, CandidateService $candidateService,
StudentService $studentService, CalendarEventService $calendarEventService): Response
{
$students = $studentService->getBaseService()->getAll();
$persons = array_map(function (Student $student) {
return $student->getPerson();
}, $students);
$candidatesByStudentAndCalendarEvent =
$candidateService->getCandidatesGroupedByStudentsAndCalendarEvents();
$calendarEvents = $calendarEventService->getAll();
$calendarEvents = Other::getIdIndexedEntityArray($calendarEvents);
$earnLevelCandidates = $candidateService
->getPersonsLastCandidateWithEarnLevelCalendarEvent($persons);
return $this->render('admin/student/students.html.twig', [
'students' => $students,
"candidatesByStudentAndCalendarEvent" => $candidatesByStudentAndCalendarEvent,
"calendarEvents" => $calendarEvents,
"earnLevelCandidates" => $earnLevelCandidates,
]);
}
public function learners(Request $request, CandidateService $candidateService,
StudentService $studentService, CalendarEventService $calendarEventService,
PersonService $personService): Response
{
$items = $personService->getLearners();
$candidatesByStudentAndCalendarEvent =
$candidateService->getCandidatesGroupedByStudentsAndCalendarEvents();
$calendarEvents = $calendarEventService->getAll();
$calendarEvents = Other::getIdIndexedEntityArray($calendarEvents);
$earnLevelCandidates = $candidateService
->getPersonsLastCandidateWithEarnLevelCalendarEvent($items);
$itemsByStatusCount = [];
foreach (Status::getAsArray() as $status) {
$itemsByStatusCount[$status] = [
"status" => $status,
"statusText" => Status::getText($status),
"count" => 0,
];
}
$itemsByStatusCount["subscriber_virtual_status"] = [
"status" => "subscriber_virtual_status",
"statusText" => "Подписчик",
"count" => 0,
];
foreach ($items as $item) {
$status = $item->isIsStudent() ? $item->getStudent()->getStatus() : "subscriber_virtual_status";
$itemsByStatusCount[$status]["count"]++;
}
$itemsByStudentLevelCount = [];
for ($i = 0; $i <= 6; $i++) {
$itemsByStudentLevelCount[$i] = [
"level" => $i,
"count" => 0,
];
}
foreach ($items as $item) {
if ($item->isIsStudent() && Status::isListedStatus($item->getStudent()->getStatus())) {
$level = $item->getStudent()->getLevel();
if ($level !== null) {
if ($level < 0) {
dd($item);
}
$itemsByStudentLevelCount[$level]["count"]++;
}
}
}
//sort by key, Status::canSendCampaignStatuses(), and rest statuses
$sorted = [];
foreach (Status::getCanSendCampaignStatuses() as $status) {
if (isset($itemsByStatusCount[$status])) {
$sorted[$status] = $itemsByStatusCount[$status];
}
}
foreach ($itemsByStatusCount as $status => $itemByStatusCount) {
if (!isset($sorted[$status])) {
$sorted[$status] = $itemByStatusCount;
}
}
$itemsByStatusCount = $sorted;
$itemsWithCanSendCampaignStatusCount = 0;
foreach ($items as $item) {
$status = $item->isIsStudent() ? $item->getStudent()->getStatus() : null;
if ($status && Status::isStudentStatus($status)) {
$itemsWithCanSendCampaignStatusCount++;
}
}
$allLeavedStudentsCount = 0;
foreach ($items as $item) {
$status = $item->isIsStudent() ? $item->getStudent()->getStatus() : null;
if ($status && Status::isLeavedStatus($status)) {
$allLeavedStudentsCount++;
}
}
$allStudentsCount = 0;
foreach ($items as $item) {
if ($item->isIsStudent()) {
$allStudentsCount++;
}
}
return $this->render('admin/student/learners.html.twig', [
'items' => $items,
"candidatesByStudentAndCalendarEvent" => $candidatesByStudentAndCalendarEvent,
"calendarEvents" => $calendarEvents,
"earnLevelCandidates" => $earnLevelCandidates,
"itemsByStatusCount" => $itemsByStatusCount,
"itemsWithCanSendCampaignStatusCount" => $itemsWithCanSendCampaignStatusCount,
"allStudentsCount" => $allStudentsCount,
"allLeavedStudentsCount" => $allLeavedStudentsCount,
"itemsByStudentLevelCount" => $itemsByStudentLevelCount,
]);
}
public function edit(Request $request, StudentService $studentService): Response
{
$id = $request->get('id');
/**
* @var Student $item
*/
$item = $id ? $studentService->getBaseService()->get($id) : null;
$person = $item ? $item->getPerson() : null;
$isNew = false;
if (!$item) {
$person = new Person();
$item = (new Student());
$isNew = true;
}
$form = $this->createForm(StudentType::class, $item);
$form->handleRequest($request);
$personForm = $this->createForm(PersonType::class, $person);
$personForm->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->persist($person);
$this->em->flush();
$item->setPerson($person);
$this->em->persist($item);
$this->em->flush();
if ($isNew) {
$studentLevel = (new StudentLevel())
->setStudent($item)
->setValue((int)$request->get("student")['level'])
->setComment('Уровень при добавлении ученика')
->setAuthor($this->getUser());
$this->em->persist($studentLevel);
$this->em->flush();
}
return $this->redirectToRoute('moderator_students');
}
return $this->render('admin/student/student_edit.html.twig', [
'form' => $form->createView(),
'personForm' => $personForm->createView(),
'item' => $item,
]);
}
public function learnerEdit(Request $request,
PersonService $personService, StudentLevelService $studentLevelService,
JobService $jobService): Response
{
$id = $request->get('id');
/**
* @var Person $person
*/
$person = $id ? $personService->getBaseService()->get($id) : null;
/**
* @var Student $student
*/
$student = $person ? $person->getStudent() : null;
$isNew = false;
if (!$person) {
$person = new Person();
$isNew = true;
}
if (!$student) {
$student = (new Student());
$isNew = true;
}
$oldStatus = $student->getStatus();
$oldPhone = $person->getPhone();
$form = $this->createForm(StudentType::class, $student);
$form->handleRequest($request);
$personForm = $this->createForm(PersonType::class, $person);
$personForm->handleRequest($request);
$virtualStatus = null;
try {
if ($form->isSubmitted()) {
$virtualStatus = $request->get("person")['virtualStudentStatus'] ?? null;
if ($virtualStatus === VirtualStatus::VIRTUAL_STATUS_SUBSCRIBER) {
$person->setIsSubscriber(true);
$person->setIsStudent(false);
} elseif (Status::isCorrect($virtualStatus)) {
$person->setIsStudent(true);
$person->setIsSubscriber(false);
$student->setStatus($virtualStatus);
} else {
throw new \Exception("Unknown: " . $virtualStatus);
}
}
if ($form->isSubmitted() && $form->isValid() && $personForm->isValid()) {
//todo redo
$newJobName = $request->get("newJobName");
$jobId = (int)($request->get("person")['job'] ?? null);
$addNewJob = $jobId === -100;
if ($addNewJob) {
if (!$newJobName) {
throw new \Exception('Название новой деятельности не может быть пустым.');
}
$job = $jobService->createOrGetDefault(["name" => $newJobName]);
} else {
$job = $jobService->getBaseService()->get($jobId);
}
$person->setJob($job);
if ($person->isIsStudent() && $studentLevelService->calcStudentLevel($student) == -1) {
throw new \Exception('Нельзя через редактор восстановить статус ученика подписчику.');
}
//обнуение студента при создании подписчика
if ($person->isIsSubscriber() && $isNew) {
$student = null;
$person->setStudent(null);
}
if ($personService->hasSameDefault($person)) {
throw new \Exception('Учащийся с такими данными уже существует.');
}
$this->em->persist($person);
$this->em->flush();
if ($isNew && $student && !$student->getId()) {
$student->setPerson($person)
->setLevel((int)$request->get("student")['level']);
}
if ($student && $student->getLevel() === null) {
$student->setLevel(0);
}
//задание старого статуса студента при переводе студента в
//подписчики или после редактирования подписчика,
//который ранее был студентом
if ($student && $oldStatus && !$person->isIsStudent() && !$isNew) {
$student->setStatus($oldStatus);
}
//уровень студента -1 при переводе из студента в подписчики
if ($person->isIsSubscriber() && $student && $student->getLevels()->count()) {
$level = $studentLevelService->calcStudentLevel($student);
if ($level > -1) {
$studentLevel = (new StudentLevel())
->setStudent($student)
->setValue($studentLevelService->calcStudentLevel($student) * -1 - 1)
->setComment('Перевод в подписчики')
->setAuthor($this->getUser());
$this->em->persist($studentLevel);
}
//если по какой-то причине уровень неверно рассчитан
if ($student && $student->getLevel() != -1) {
$student->setLevel($studentLevelService->calcStudentLevel($student));
$this->em->flush();
}
}
if ($oldPhone && $oldPhone != $person->getPhone()) {
$person->setTelegramUserId(null);
}
if ($person->isIsStudent()) {
$student->setPerson($person);
if ($student->getLevel() === null) {
$student->setLevel(0);
}
$this->em->persist($student);
$this->em->flush();
}
if ($isNew) {
if ($person->isIsStudent()) {
$studentLevel = (new StudentLevel())
->setStudent($student)
->setValue((int)$request->get("student")['level'])
->setComment('Уровень при добавлении ученика')
->setAuthor($this->getUser());
$this->em->persist($studentLevel);
$this->em->flush();
}
}
return $this->redirectToRoute('moderator_learners');
}
} catch (\Throwable $e) {
$this->addFlash('errors', $e->getMessage());
}
return $this->render('admin/student/learner_edit.html.twig', [
'form' => $form->createView(),
'personForm' => $personForm->createView(),
'item' => $student,
'person' => $person,
]);
}
public function delete(Request $request, StudentService $studentService,
PersonService $personService): RedirectResponse
{
$id = $request->get('id');
if (!$id) {
$this->addFlash('errors', 'Не указан идентификатор учащегося.');
return $this->redirectToRoute('moderator_learners');
}
/** @var Person|null $item */
$item = $personService->getBaseService()->get($id);
if (!$item) {
$this->addFlash('errors', 'Учащийся не найден.');
return $this->redirectToRoute('moderator_learners');
}
try {
$item->setStatus(Status::STATUS_DELETED);
if ($item->getStudent()) {
$item->getStudent()->setStatus(Status::STATUS_DELETED);
}
$this->em->flush();
$studentName = $item->getName();
$this->addFlash('success', 'Учащийся ' . $studentName . ' помечен как удалённый.');
} catch (\Throwable $e) {
$this->addFlash('errors', 'Ошибка при удалении учащегося. ' . $e->getMessage() . '.');
}
return $this->redirectToRoute('moderator_learners');
}
public function registration(Request $request, PersonService $personService,
CalendarEventService $calendarEventService, CandidateService $candidateService): Response
{
$calendarEventId = $request->query->get('calendarEventId');
$selectedCalendarEvent = null;
$students = [];
// Get all calendar events without review requirement
$allCalendarEvents = $calendarEventService->getAll();
$calendarEvents = array_filter($allCalendarEvents, function($event) {
return !$event->isIsCandidateReviewRequired();
});
if ($calendarEventId) {
/** @var CalendarEvent $selectedCalendarEvent */
$selectedCalendarEvent = $calendarEventService->getBaseService()->get($calendarEventId);
if ($selectedCalendarEvent) {
// Get students with matching level
$fromLevel = $selectedCalendarEvent->getFromLevel();
$allStudents = $personService->getLearners();
$students = array_filter($allStudents, function($person) use ($fromLevel, $selectedCalendarEvent) {
if (!Status::isCanSendCampaignStatus($person->getVirtualStudentStatus())) {
return false;
}
if ($selectedCalendarEvent->getRequireStudentStatus()
&& $person->getVirtualStudentStatus() != $selectedCalendarEvent->getRequireStudentStatus()) {
return false;
}
if ($selectedCalendarEvent->getFromLevel() === null) {
return true;
}
if ($person->isIsStudent() && $person->getStudent()) {
$level = $person->getStudent()->getLevel();
return $level !== null && $level >= $fromLevel
&& ($selectedCalendarEvent->getToLevel() === null || $level <= $selectedCalendarEvent->getToLevel())
&& $person->isCalendarEventStatusAllowed($selectedCalendarEvent);
}
return false;
});
// Get existing candidates for this event
$existingCandidates = $candidateService->getDefault([
'calendarEvent' => $selectedCalendarEvent,
'status' => [\App\Enum\Candidate\Status::STATUS_NEW,
\App\Enum\Candidate\Status::STATUS_APPROVED,
\App\Enum\Candidate\Status::STATUS_DRAFT]
]);
$candidatesByPerson = [];
foreach ($existingCandidates as $candidate) {
if ($candidate->getPerson()) {
$candidatesByPerson[$candidate->getPerson()->getId()] = $candidate;
}
}
}
}
// Prepare dropdown data for calendar events
$routeParamDropdownsData = [
'calendarEvent' => array_map(function($event) {
return [
'listItemId' => $event->getId(),
'text' => $event->getText(['quotes' => true])
];
}, array_values($calendarEvents))
];
return $this->render('admin/student/registration.html.twig', [
'students' => $students,
'calendarEvents' => $calendarEvents,
'selectedCalendarEvent' => $selectedCalendarEvent,
'routeParamDropdownsData' => $routeParamDropdownsData,
'candidatesByPerson' => $candidatesByPerson ?? []
]);
}
public function toggleRegistration(Request $request, CandidateService $candidateService,
PersonService $personService, CalendarEventService $calendarEventService,
UserService $userService): JsonResponse
{
return \App\Library\Utils\ApiHandler::handleApiRequest($request, function (&$responseCode, $content) use (
$candidateService, $personService, $calendarEventService, $userService
) {
$personId = $content['person_id'] ?? null;
$calendarEventId = $content['calendar_event_id'] ?? null;
if (!$personId || !$calendarEventId) {
$responseCode = 400;
return ['error' => 'Missing person_id or calendar_event_id'];
}
/** @var Person $person */
$person = $personService->getBaseService()->get($personId);
/** @var CalendarEvent $calendarEvent */
$calendarEvent = $calendarEventService->getBaseService()->get($calendarEventId);
if (!$person || !$calendarEvent) {
$responseCode = 404;
return ['error' => 'Person or CalendarEvent not found'];
}
// Check if candidate already exists
$existingCandidates = $candidateService->getDefault([
'person' => $person,
'calendarEvent' => $calendarEvent,
]);
/** @var Candidate $existingCandidate */
$existingCandidate = count($existingCandidates) > 0 ? $existingCandidates[0] : null;
$existsWithStatusNotNew = $existingCandidate && $existingCandidate->getStatus() !== \App\Enum\Candidate\Status::STATUS_NEW;
if ($existsWithStatusNotNew && $calendarEvent->isIsCandidateReviewRequired()) {
$responseCode = 400;
return ['error' => 'Статус кандидата: ' . $existingCandidate->getStatusText()];
}
if ($existingCandidate) {
// Delete candidate
$this->em->remove($existingCandidate);
$this->em->flush();
$responseCode = 200;
return ['action' => 'deleted', 'candidateId' => null];
} else {
// Create new candidate
$candidate = new \App\Entity\Candidate();
$candidate->setPerson($person);
$candidate->setCalendarEvent($calendarEvent);
$candidate->setAuthor($this->getUser());
$candidate->setCurator($userService->getStarCurator());
$candidate->setStatus(\App\Enum\Candidate\Status::STATUS_APPROVED);
$candidate->setUpdatedAt(new \DateTime());
$this->em->persist($candidate);
$this->em->flush();
$responseCode = 200;
return ['action' => 'created', 'candidateId' => $candidate->getId()];
}
}, true, ['person_id', 'calendar_event_id']);
}
public function loadTelegramHistory(Request $request, PersonService $personService,
PyTgUtils $pyTgUtils): JsonResponse
{
return \App\Library\Utils\ApiHandler::handleApiRequest($request, function (&$responseCode, $content) use (
$personService, $pyTgUtils
) {
$personId = $content['person_id'] ?? null;
if (!$personId) {
$responseCode = 400;
return ['error' => 'Missing person_id'];
}
/** @var Person $person */
$person = $personService->getBaseService()->get($personId);
if (!$person) {
$responseCode = 404;
return ['error' => 'Person not found'];
}
$telegramUserId = $person->getTelegramUserId();
if (!$telegramUserId) {
$responseCode = 400;
return ['error' => 'Telegram user ID not set'];
}
try {
// Устанавливаем технический аккаунт администратора
$pyTgUtils->setTechAdminAccount();
// Создаем приватный чат
$chat = $pyTgUtils->pyTg->createPrivateChat($telegramUserId);
$chatId = $chat['id'];
// Получаем последние 5 сообщений
$messages = $pyTgUtils->pyTg->getChatHistory($chatId, 10);
$messages = array_map(function (Message $message) {
return $message->getData();
}, $messages);
// Обрабатываем сообщения
$processedMessages = [];
$debugInfo = [];
foreach ($messages as $messageIndex => $message) {
$processedMessage = [
'id' => $message['id'] ?? null,
'date' => $message['date'] ?? null,
'is_outgoing' => $message['is_outgoing'] ?? false,
'text' => '',
'images' => [],
'debug' => [] // Отладочная информация (будет удалена перед отправкой)
];
$messageDebug = [
'message_index' => $messageIndex,
'message_id' => $message['id'] ?? null,
'content_type' => $message['content']['@type'] ?? 'unknown',
];
// Получаем текст сообщения
if (isset($message['content'])) {
if (isset($message['content']['text']) && isset($message['content']['text']['text'])) {
$processedMessage['text'] = $message['content']['text']['text'];
} elseif (isset($message['content']['caption']) && isset($message['content']['caption']['text'])) {
$processedMessage['text'] = $message['content']['caption']['text'];
}
// Обрабатываем фото
if (isset($message['content']['@type']) && $message['content']['@type'] === 'messagePhoto') {
$messageDebug['has_photo'] = true;
if (isset($message['content']['photo']['sizes'])) {
$sizes = $message['content']['photo']['sizes'];
$messageDebug['sizes_count'] = count($sizes);
// Берем изображение среднего размера (для оптимизации)
// Если размеров меньше 3, берем последнее
$photoIndex = count($sizes) > 2 ? count($sizes) - 2 : count($sizes) - 1;
$selectedPhoto = $sizes[$photoIndex];
$messageDebug['selected_photo_index'] = $photoIndex;
$messageDebug['selected_photo_type'] = $selectedPhoto['type'] ?? 'unknown';
$imageLoaded = false;
$imagePath = null;
// Проверяем, есть ли локальный путь к файлу
if (isset($selectedPhoto['photo']['local']['path']) &&
file_exists($selectedPhoto['photo']['local']['path'])) {
$imagePath = $selectedPhoto['photo']['local']['path'];
$messageDebug['source'] = 'local_path';
$messageDebug['path'] = $imagePath;
$imageLoaded = true;
} elseif (isset($selectedPhoto['photo']['id'])) {
// Если файл не загружен локально, загружаем через API
$messageDebug['source'] = 'api_download';
$messageDebug['file_id'] = $selectedPhoto['photo']['id'];
try {
// downloadFile возвращает base64 данные напрямую
$base64Data = $pyTgUtils->pyTg->downloadFile($selectedPhoto['photo']['id']);
$messageDebug['download_response'] = [
'is_string' => is_string($base64Data),
'data_length' => is_string($base64Data) ? strlen($base64Data) : 0,
];
if (is_string($base64Data) && !empty($base64Data)) {
// Данные уже в base64, используем их напрямую
$processedMessage['images'][] = "data:image/jpeg;base64," . $base64Data;
$messageDebug['base64_length'] = strlen($base64Data);
$messageDebug['success'] = true;
$imageLoaded = true; // Помечаем, что изображение загружено
} else {
$messageDebug['error'] = 'Invalid base64 data received';
}
} catch (\Exception $e) {
$messageDebug['error'] = 'Download failed: ' . $e->getMessage();
}
}
// Конвертируем изображение в base64 (только для локальных файлов)
if ($imageLoaded && $imagePath) {
try {
$fileSize = filesize($imagePath);
$messageDebug['file_size'] = $fileSize;
// Ограничение размера файла (5 МБ)
if ($fileSize > 5 * 1024 * 1024) {
$messageDebug['error'] = 'File too large: ' . $fileSize . ' bytes';
} else {
$imageData = file_get_contents($imagePath);
if ($imageData !== false) {
$base64 = base64_encode($imageData);
$mimeType = mime_content_type($imagePath);
$processedMessage['images'][] = "data:$mimeType;base64,$base64";
$messageDebug['mime_type'] = $mimeType;
$messageDebug['base64_length'] = strlen($base64);
$messageDebug['success'] = true;
} else {
$messageDebug['error'] = 'Failed to read file contents';
}
}
} catch (\Exception $e) {
$messageDebug['error'] = 'Base64 conversion failed: ' . $e->getMessage();
}
}
} else {
$messageDebug['error'] = 'No photo sizes found';
}
}
}
$debugInfo[] = $messageDebug;
// Удаляем debug перед добавлением в результат
unset($processedMessage['debug']);
$processedMessages[] = $processedMessage;
}
// Сортируем сообщения по дате (от старых к новым)
usort($processedMessages, function($a, $b) {
return ($a['date'] ?? 0) - ($b['date'] ?? 0);
});
$responseCode = 200;
return [
'success' => true,
'messages' => $processedMessages,
'person_name' => $person->getName(),
'debug' => $debugInfo, // Отладочная информация
'total_messages' => count($processedMessages),
'messages_with_images' => count(array_filter($processedMessages, function($m) {
return !empty($m['images']);
}))
];
} catch (\Exception $e) {
$responseCode = 500;
return ['error' => 'Failed to load Telegram history: ' . $e->getMessage()];
}
}, true, ['person_id']);
}
}