Compare commits

..

19 Commits

Author SHA1 Message Date
beebd6f31f Hotfix иконки
All checks were successful
Build and deploy / Publish image (push) Successful in 6m53s
2025-04-01 14:58:33 +03:00
d67683aecd UI фиксы
All checks were successful
Build and deploy / Publish image (push) Successful in 5m43s
2025-04-01 14:38:48 +03:00
e519a4d126 Небольшие UI фиксы
All checks were successful
Build and deploy / Publish image (push) Successful in 3m33s
2025-03-30 23:56:34 +03:00
724062e7b3 Установил альтернативную отрисовку по умолчанию для первого апреля
All checks were successful
Build and deploy / Publish image (push) Successful in 5m49s
2025-03-30 23:15:35 +03:00
255c372709 Social Media Preview Fix
All checks were successful
Build and deploy / Publish image (push) Successful in 18m15s
2025-03-17 21:40:38 +03:00
5844bcae1e Добавил посхалку на 1е апреля
All checks were successful
Build and deploy / Publish image (push) Successful in 1m51s
2025-03-10 04:54:10 +03:00
e090e7f834 Выход из альфы 2025-03-10 03:58:54 +03:00
4c892f6240 Добавил альтернативный режим рендеринга PDF
All checks were successful
Build and deploy / Publish image (push) Successful in 4m32s
2025-03-10 03:53:05 +03:00
3f5c3d9681 Добавил уведомление о том что нельзя распечатать или отправить
All checks were successful
Build and deploy / Publish image (push) Successful in 4m37s
2025-03-10 02:20:03 +03:00
c907dd769e Переделал кнопку info
All checks were successful
Build and deploy / Publish image (push) Successful in 1m51s
2025-03-10 01:23:58 +03:00
c4520c2821 Небольшие фиксы
All checks were successful
Build and deploy / Publish image (push) Successful in 4m33s
2025-03-10 01:06:34 +03:00
14915856e8 db hotfix
All checks were successful
Build and deploy / Publish image (push) Successful in 1m52s
2025-03-10 00:30:54 +03:00
c1b9fd3949 Отключил другие институты
All checks were successful
Build and deploy / Publish image (push) Successful in 1m51s
2025-03-10 00:20:12 +03:00
fb892bd124 Фикс прозрачности институтов
All checks were successful
Build and deploy / Publish image (push) Successful in 1m50s
2025-03-10 00:13:20 +03:00
6e06142e7a Добавил зал славы
All checks were successful
Build and deploy / Publish image (push) Successful in 6m28s
2025-03-09 23:59:32 +03:00
a5426faa2e Добавил мемы
All checks were successful
Build and deploy / Publish image (push) Successful in 10m51s
2025-03-09 22:57:01 +03:00
613334e2d2 Добавил задачи
All checks were successful
Build and deploy / Publish image (push) Successful in 42s
2025-02-06 07:56:30 +03:00
cd2adf18e5 Изменил папку нахождения бд
All checks were successful
Build and deploy / Publish image (push) Successful in 1m6s
2025-02-06 07:50:59 +03:00
2e5fb3972e Добавил выбор направления и другие мелкие фиксы 2025-02-06 07:14:44 +03:00
40 changed files with 642 additions and 144 deletions

4
.gitignore vendored
View File

@@ -501,3 +501,7 @@ src/Otchinslator/appsettings.Development.json
src/.idea/.idea.Otchinslator/.idea/
src/.vscode/
src/Otchinslator/wwwroot/img/memes/
src/Otchinslator/wwwroot/img/notifications/

View File

@@ -2,6 +2,13 @@
Веб приложение отчислятора 3000
# TODO
- [ ] Сделать обновление валидации от radiobutton
- [ ] Добавить направления других институтов
- [ ] Найти шаблоны других институтов и написать выбор шаблонов
- [x] Рефактор кода
# Лицензия
```

View File

@@ -6,6 +6,9 @@ services:
- "8025:8080"
volumes:
- /srv/otchislator/pdfs:/app/PDFCache
- /srv/otchislator/data:/app/data
- /srv/otchislator/memes:/app/wwwroot/img/memes
- /srv/otchislator/notifications:/app/wwwroot/img/notifications
environment:
- BOT_TOKEN=123:ABC
- ChatId=

View File

@@ -3,9 +3,9 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<base href="/"/>
@* TODO: скачать шрифты *@
<meta property="og:image" content="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/styles.css" asp-append-version="true" />

View File

@@ -7,7 +7,7 @@
{
{ "Анкета", "/questionnaire" },
{ "Заявление", "/statement" },
{ "Отправка", "/otchislenie/result" },
{ "Отправка", "/result" },
{ "Свобода", "/otchislenie/congratulation" }
};
@@ -23,7 +23,7 @@
case "statement":
_lvl = 2;
break;
case "otchislenie/result":
case "result":
_lvl = 3;
break;
case "otchislenie/congratulation":

View File

@@ -0,0 +1,175 @@
@page "/Achievements"
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<PageTitle>Зал славы</PageTitle>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #111;
overflow: hidden;
overflow-y: hidden;
flex-direction: column;
}
.carousel-container {
position: relative;
height: 400px;
transform: rotate(20deg);
margin: 20px 0;
}
.carousel {
display: flex;
width: max-content;
flex-wrap: nowrap;
animation: scroll 15s linear infinite;
}
.carousel.reverse {
animation: scroll-reverse 15s linear infinite;
}
.card {
width: 213px;
height: 300px;
background: #222;
margin: 0 10px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
}
.card img {
width: 210px;
height: 297px;
object-fit: cover;
border-radius: 5px;
}
@@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-10%);
}
}
@@keyframes scroll-reverse {
0% {
transform: translateX(-10%);
}
100% {
transform: translateX(0);
}
}
.hall-of-fame {
z-index: 9999;
position: absolute;
top: 10px;
right: 20px;
font-size: 3vw;
color: white;
font-weight: bold;
background-color: #444;
border-radius: 9999px;
padding: 2px 15px;
}
.back-button {
z-index: 9999;
position: absolute;
bottom: 10px;
left: 20px;
font-size: 2vw;
color: white;
border: none;
cursor: pointer;
background-color: #444;
border-radius: 9999px;
padding: 4px 20px;
}
/* Адаптивность для маленьких экранов */
@@media (max-width: 600px) {
.hall-of-fame {
font-size: 5vw;
padding: 5px 20px;
}
.back-button {
font-size: 4vw;
padding: 6px 25px;
}
}
/* Адаптивность для средних экранов */
@@media (min-width: 601px) and (max-width: 1024px) {
.hall-of-fame {
font-size: 4vw;
padding: 4px 18px;
}
.back-button {
font-size: 3vw;
padding: 5px 22px;
}
}
.back-button:hover {
background-color: #666;
}
</style>
<div class="hall-of-fame">Зал славы</div>
<a class="back-button" href="/">Назад</a>
<div class="carousel-container">
<div class="carousel">
<div class="card"><img src="/img/notifications/1.png" alt="Image 1"></div>
<div class="card"><img src="/img/notifications/2.png" alt="Image 2"></div>
<div class="card"><img src="/img/notifications/3.png" alt="Image 3"></div>
<div class="card"><img src="/img/notifications/4.png" alt="Image 4"></div>
<div class="card"><img src="/img/notifications/5.png" alt="Image 5"></div>
<div class="card"><img src="/img/notifications/6.png" alt="Image 6"></div>
<div class="card"><img src="/img/notifications/7.png" alt="Image 7"></div>
<div class="card"><img src="/img/notifications/8.png" alt="Image 8"></div>
<div class="card"><img src="/img/notifications/8.png" alt="Image 9"></div>
<div class="card"><img src="/img/notifications/9.png" alt="Image 10"></div>
</div>
</div>
<div class="carousel-container">
<div class="carousel reverse">
<div class="card"><img src="/img/notifications/10.png" alt="Image 1"></div>
<div class="card"><img src="/img/notifications/11.png" alt="Image 2"></div>
<div class="card"><img src="/img/notifications/12.png" alt="Image 3"></div>
<div class="card"><img src="/img/notifications/13.png" alt="Image 4"></div>
<div class="card"><img src="/img/notifications/14.png" alt="Image 5"></div>
<div class="card"><img src="/img/notifications/15.png" alt="Image 6"></div>
<div class="card"><img src="/img/notifications/16.png" alt="Image 7"></div>
<div class="card"><img src="/img/notifications/17.png" alt="Image 8"></div>
<div class="card"><img src="/img/notifications/18.png" alt="Image 9"></div>
<div class="card"><img src="/img/notifications/19.png" alt="Image 10"></div>
</div>
</div>
<div class="carousel-container">
<div class="carousel">
<div class="card"><img src="/img/notifications/20.png" alt="Image 1"></div>
<div class="card"><img src="/img/notifications/21.png" alt="Image 2"></div>
<div class="card"><img src="/img/notifications/22.png" alt="Image 3"></div>
<div class="card"><img src="/img/notifications/23.png" alt="Image 4"></div>
<div class="card"><img src="/img/notifications/24.png" alt="Image 5"></div>
<div class="card"><img src="/img/notifications/25.png" alt="Image 6"></div>
<div class="card"><img src="/img/notifications/26.png" alt="Image 7"></div>
<div class="card"><img src="/img/notifications/27.png" alt="Image 8"></div>
<div class="card"><img src="/img/notifications/28.png" alt="Image 9"></div>
<div class="card"><img src="/img/notifications/29.png" alt="Image 10"></div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
export class Achievements {
}
window.Achievements = Achievements;

View File

@@ -8,6 +8,18 @@
<PageTitle>Институт | Отчислятор 3000</PageTitle>
<PageScript Src="./Components/Pages/ChooseInstitut.razor.js"/>
<dialog id="notready_modal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="text-lg font-bold">Сервис отчислений</h3>
<p class="py-4">Для этого института ещё не был добавлен шаблон заявления, если Вы знаете где его найти и кому направлять заявления, свяжитесь с разработчиком<br><br>ТГ: <a target="_blank" href="https://t.me/serega404">@@serega404</a></p>
<div class="modal-action">
<form method="dialog">
<button class="btn">Закрыть</button>
</form>
</div>
</div>
</dialog>
<div class="relative w-96 sm:w-[32rem]">
<div
class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/4 transform -translate-x-1/2 italic">
@@ -16,20 +28,20 @@
<div class="flex flex-col space-y-4 w-96 sm:w-[32rem]">
<div class="card rounded-badge bg-base-200 p-4">
<div class="grid grid-cols-2 gap-4 p-4">
<button id="ИКТИБ" class="institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/iktib.jpg" alt="ИКТИБ" class="w-full h-32 object-contain rounded-md" draggable="false">
<button id="1" class="institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/iktib.png" alt="ИКТИБ" class="w-full h-32 object-contain rounded-md" draggable="false">
<p class="text-center mt-2">ИКТИБ</p>
</button>
<button id="ИРТСУ" class="institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/irtsu.jpg" alt="ИРТСУ" class="w-full h-32 object-contain rounded-md" draggable="false">
<button id="2" onclick="notready_modal.showModal()" class="dis-institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/irtsu.png" alt="ИРТСУ" class="w-full h-32 object-contain rounded-md" draggable="false">
<p class="text-center mt-2">ИРТСУ</p>
</button>
<button id="ИНЭП" class="institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/inep.jpg" alt="ИНЭП" class="w-full h-32 object-contain rounded-md" draggable="false">
<button id="3" onclick="notready_modal.showModal()" class="dis-institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/inep.png" alt="ИНЭП" class="w-full h-32 object-contain rounded-md" draggable="false">
<p class="text-center mt-2">ИНЭП</p>
</button>
<button id="ИУЭС" class="institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/iues.jpg" alt="ИУЭС" class="w-full h-32 object-contain rounded-md" draggable="false">
<button id="4" onclick="notready_modal.showModal()" class="dis-institut-button btn h-auto card bg-white p-4 select-none">
<img src="img/iues.png" alt="ИУЭС" class="w-full h-32 object-contain rounded-md" draggable="false">
<p class="text-center mt-2">ИУЭС</p>
</button>
</div>

View File

@@ -3,14 +3,16 @@
@using Otchinslator.Components.Layout
@layout OtchislenieLayout
@attribute [Authorize]
@using BlazorPageScript
<PageTitle>Поздравляем! | Отчислятор 3000</PageTitle>
<PageScript Src="./Components/Pages/Congratulation.razor.js"/>
<div>
<div class="text-center mx-auto mb-4 font-bold text-4xl md:text-5xl w-max justify-center italic">Поздравляем!</div>
<img class="w-96 h-96 mx-auto" src="img/party-popper.svg" alt=""/>
<div class="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0 mt-4">
<a href="/otchislenie/result" class="btn w-full lg:w-48 h-12 btn-primary rounded-full text-2xl">
<a href="/result" class="btn w-full lg:w-48 h-12 btn-primary rounded-full text-2xl">
Назад
</a>
<a href="/" class="btn w-full lg:w-48 h-12 btn-primary rounded-full text-2xl">

View File

@@ -0,0 +1,10 @@
export function onLoad() {
const url = '/getStatement';
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
iframe.onload = function () {
iframe.contentWindow.print();
};
}

View File

@@ -0,0 +1,112 @@
@page "/fstaprl"
@inject AuthenticationStateProvider AuthenticationStateProvider
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using NPetrovich
@using Otchinslator.Services
@attribute [Authorize]
@inject IStatementGenerator StatementGenerator
<PageTitle>Заяление отправлено | 1 апреля</PageTitle>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100dvh;
}
</style>
@code {
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var userEmail = authState.User.Identity.Name;
var userFIO = authState.User.Claims.FirstOrDefault(x => x.Type == "name")?.Value;
var petrovich = new Petrovich()
{
FirstName = userFIO.Split(' ')[0],
LastName = userFIO.Split(' ')[1],
MiddleName = userFIO.Split(' ').Length > 2 ? userFIO.Split(' ')[2] : "",
Gender = Gender.Male
};
userFIO = petrovich.InflectFirstNameTo(Case.Genitive) + " " + petrovich.InflectLastNameTo(Case.Genitive) + " " + petrovich.InflectMiddleNameTo(Case.Genitive);
var userData = new UserData
{
reason = "Прошу отчислить меня по собственному желанию в связи с не желанием продолжать обучение в данном учебном заведении, и планами перевестись в ДГТУ.",
email = userEmail,
phone = "",
fio = userFIO,
kurs = 1,
isFreeEducation = true,
isOchno = true,
speciality = SpecialityType.Bakalavriat,
direction = ""
};
var statement = await StatementGenerator.GenerateStatementAsync(userData, "frstaprl.docx");
var pdf = await StatementGenerator.ConvertToPDFAsync(statement);
await using var fileStream = new FileStream("./PDFCache/" + userEmail.Split('@')[0] + ".pdf", FileMode.Create, FileAccess.Write);
await pdf.CopyToAsync(fileStream);
}
}
<dialog id="denyModal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="text-lg font-bold text-center">С 1 апреля!</h3>
<img src="https://risovach.ru/upload/2013/04/mem/moe-lico_15228723_orig_.jpeg" class="h-80 my-2 mx-auto"
alt=""/>
<div class="modal-action justify-center">
<form method="dialog">
<button class="btn btn-primary">Понял, принял</button>
</form>
</div>
</div>
</dialog>
<div class="relative flex flex-col items-center justify-center">
<div class="text-center font-bold text-3xl md:text-5xl w-max italic mb-3">
<br>Заявление на отчисление<p class="text-2xl font-normal">отправлено директору</p>
</div>
<div id="pdfrenderer" class="flex justify-center">
</div>
<div class="join w-96 sm:w-[27rem] mt-4 flex gap-2 justify-center">
<button onclick="denyModal.showModal()" class="btn btn-error rounded-full flex-grow w-30 flex items-center justify-center">
Отозвать
</button>
<a id="downloadPDF" target="_blank" href="/getStatement"
class="btn btn-primary bg-base-200 border-base-200 rounded-full flex-grow-0 w-[3rem] flex items-center justify-center relative">
<div class="absolute inset-0 flex items-center justify-center">
<img class="p-2" src="img/pdf.svg" alt=""/>
</div>
</a>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>
<script>
const pdfrenderer = document.getElementById('pdfrenderer');
const url = '/getStatement';
pdfrenderer.innerHTML = '<canvas id="pdf-canvas" class="w-96 sm:w-[27rem] rounded-2xl justify-center"></canvas>';
pdfjsLib.getDocument(url).promise.then(function (pdf) {
pdf.getPage(1).then(function (page) {
const viewport = page.getViewport({scale: 1});
const canvas = document.getElementById('pdf-canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
const renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext);
});
});
</script>

View File

@@ -1,18 +1,21 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthenticationStateProvider
@page "/"
@attribute [DiscoverCollocatedJS]
<PageTitle>Отчислятор 3000</PageTitle>
<JS For="this" Args="[AuthenticationStateProvider.GetAuthenticationStateAsync().Result.User.Identity?.IsAuthenticated]" />
<JS For="this"
Args="[AuthenticationStateProvider.GetAuthenticationStateAsync().Result.User.Identity?.IsAuthenticated]"/>
<div class="relative">
@* TODO: Дописать модальное окно *@
<dialog id="info_modal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="text-lg font-bold">Сервис отчислений</h3>
<p class="py-4">Привет, это сервис отчислений!<br>Автор: <a target="_blank" href="https://t.me/serega404">@@serega404</a></p>
<p class="py-4">Привет, это НЕОФИЦИАЛЬНЫЙ сервис отчислений!<br><br>Авторизуясь Вы соглашаетесь передать
личные данные, такие как email + фио (из авторизации ЮФУ), и все остальные запрошенные далее. На нашем
сервере хранятся только Ваши сгенерированные pdf заявления, другая информация сохраняется только в
браузере.<br><br>Автор: <a target="_blank" href="https://t.me/serega404">@@serega404</a></p>
<div class="modal-action">
<form method="dialog">
<button class="btn">Закрыть</button>
@@ -20,13 +23,31 @@
</div>
</div>
</dialog>
@* TODO: Исправить расположение названия *@
<div class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/3 transform -translate-x-1/2 italic"><br>Отчислятор 3000<br><p class="text-2xl text-red-500">pre-alpha</p></div>
<dialog id="meme_modal" class="modal modal-middle sm:modal-middle">
<div class="modal-box">
<h3 class="text-lg font-bold text-center">Мемная поддержка</h3>
<img id="memeImg" src="/img/memes/1.jpg" alt="meme" class="h-96 mt-2 mx-auto rounded-lg">
<div class="flex items-center mt-4">
<span class="text-gray-400 text-sm">Может содержаться ненормативная лексика!</span>
<div class="modal-action mt-0 ml-auto mr-0">
<form method="dialog">
<button class="btn btn-error">Закрыть</button>
</form>
<button id="nextmeme" class="btn btn-success">Дальше</button>
</div>
</div>
</div>
</dialog>
<div
class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/2 transform -translate-x-1/2 italic">
<br>Отчислятор 3000<br>
</div>
<div class="flex flex-col space-y-4 w-96">
<div class="card rounded-badge bg-base-200 p-4">
<h2 class="card-title text-center text-3xl justify-center my-4">Мне нужно...</h2>
@* <h2 class="card-title text-center text-3xl justify-center my-4">Мне нужно...</h2> *@
<div class="flex flex-col space-y-4 mt-1">
<button id="otchislenie" class="action-button btn h-16 btn-primary rounded-full text-2xl relative">
<button id="otchislenie"
class="action-button btn h-16 btn-primary bg-green-300 hover:bg-green-400 border-none rounded-full text-2xl relative">
Отчислиться
<div class="absolute bg-base-200 rounded-full right-1 w-14 h-14">
@{
@@ -41,40 +62,45 @@
}
</div>
</button>
<button id="downgrade" class="btn h-16 btn-primary rounded-full text-2xl relative">
Понизить курс
<div class="absolute bg-base-200 rounded-full right-1 w-14 h-14">
<div class="divider" style="margin-top: 0.25rem;"></div>
<a href="/Achievements" class="btn h-12 btn-primary rounded-full text-xl relative"
style="margin-top: 0.25rem;">
Наши достижения
<div class="absolute bg-base-200 rounded-full right-1 w-10 h-10">
<img class="p-2" src="img/down.svg" alt=""/>
</div>
<i class="absolute text-sm bottom-0 text-base-200 font-medium">временно недоступно</i>
</button>
<button id="akadem" class="btn h-16 btn-primary p-0 rounded-full text-2xl relative">
Уйти в академ
<div class="absolute bg-base-200 rounded-full right-1 w-14 h-14">
</a>
<button onclick="meme_modal.showModal()" class="btn h-12 btn-primary rounded-full text-xl relative">
Мемная помощь
<div class="absolute bg-base-200 rounded-full right-1 w-10 h-10">
<img class="p-2" src="img/akadem.svg" alt=""/>
</div>
<i class="absolute text-sm bottom-0 text-base-200 font-medium">временно недоступно</i>
</button>
<div class="divider"></div>
<div class="divider" style="margin-top: 0.25rem;"></div>
<CascadingAuthenticationState>
<AuthorizeView>
<Authorized>
<a href="MicrosoftIdentity/Account/SignOut" class="btn btn-lg btn-primary rounded-full text-2xl relative">
<a href="MicrosoftIdentity/Account/SignOut"
class="btn btn-md btn-primary rounded-full text-2xl relative"
style="margin-top: 0.25rem;">
@context.User.Identity?.Name!.Split("@")[0]
<div class="absolute bg-base-200 rounded-full right-1 w-14 h-14">
<div class="absolute bg-base-200 rounded-full right-1 w-10 h-10">
<img class="p-3" src="img/exit.svg" alt=""/>
</div>
</a>
</Authorized>
<NotAuthorized>
<a class="btn btn-lg bg-green-300 rounded-full text-2xl" href="MicrosoftIdentity/Account/SignIn">Начать</a>
<a class="btn btn-md btn-primary rounded-full text-base"
href="MicrosoftIdentity/Account/SignIn">Авторизоваться</a>
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
</div>
</div>
<div class="text-center">
<button onclick="info_modal.showModal()" class="btn btn-sm btn-primary bg-base-200 rounded-full text-1xl">?</button>
<button onclick="info_modal.showModal()" class="btn btn-sm btn-primary bg-base-200 rounded-full text-1xl">
?
</button>
</div>
</div>
</div>

View File

@@ -1,17 +1,24 @@
export default class extends BlazorJSComponents.Component {
setParameters(IsAuthenticated) {
const buttons = document.querySelectorAll('.action-button');
buttons.forEach(button => {
button.addEventListener('click', () => {
localStorage.setItem('action', button.id);
if (IsAuthenticated) {
window.location.href = '/chooseinstitut';
} else {
window.location.href = '/MicrosoftIdentity/Account/SignIn';
}
const button = document.querySelector('.action-button');
button.addEventListener('click', () => {
localStorage.setItem('action', button.id);
if (IsAuthenticated) {
window.location.href = '/chooseinstitut';
} else {
window.location.href = '/MicrosoftIdentity/Account/SignIn';
}
)
});
this.render();
}
)
const nextMemeButton = document.getElementById('nextmeme');
const nextMemeImg = document.getElementById('memeImg');
var random = Math.floor(Math.random() * 100) + 1;
nextMemeImg.src = "/img/memes/" + random + ".jpg";
nextMemeButton.addEventListener('click', () => {
random = Math.floor(Math.random() * 100) + 1;
nextMemeImg.src = "/img/memes/" + random + ".jpg";
})
}
}

View File

@@ -114,12 +114,11 @@
<div class="relative">
<div
class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/3 transform -translate-x-1/2 italic">
<br>Небольшая анкета
class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/4 transform -translate-x-1/2 italic">
<br>Кто ты воин?
</div>
<div class="flex flex-col space-y-4 w-96">
<div class="card rounded-badge bg-base-200 p-4">
<h2 class="card-title text-center text-3xl justify-center my-4">Кто ты воин?</h2>
<div class="join rounded-full justify-center">
<input class="join-item btn bg-white w-1/3" type="radio" name="options" aria-label="Баклан"/>
<input class="join-item btn bg-white w-1/3" type="radio" name="options" aria-label="Спец"/>
@@ -133,6 +132,10 @@
<input class="join-item btn bg-white w-1/5" type="radio" name="kurs" aria-label="4"/>
<input class="join-item btn bg-white w-1/5" type="radio" name="kurs" aria-label="5"/>
</div>
<h2 class="text-center text-2xl justify-center my-4">Направление</h2>
<select id="speciality" class="select rounded-full justify-center bg-white border-none">
<option value="0" disabled selected>Выберите направление</option>
</select>
<div class="flex justify-center gap-12 items-center">
<div>
<h2 class="text-center text-2xl justify-center my-4">Платник?</h2>

View File

@@ -19,6 +19,7 @@ export function onLoad() {
}
});
const specialitySelect = document.getElementById('speciality');
const continueButton = document.querySelector('a[href="/statement"]');
const options = document.querySelectorAll('input[name="options"]');
const kursElements = document.querySelectorAll('input[name="kurs"]');
@@ -31,6 +32,26 @@ export function onLoad() {
const kursElement4 = Array.from(kursElements).find(k => k.getAttribute('aria-label') === '4');
const kursElement5 = Array.from(kursElements).find(k => k.getAttribute('aria-label') === '5');
fetch('/api/getSpecialities?institutId=' + localStorage.getItem('institut'))
.then(response => response.json())
.then(data => {
const specialities = data.filter(speciality => speciality.name === 'Программная инженерия');
const otherSpecialities = data.filter(speciality => speciality.name !== 'Программная инженерия');
specialities.concat(otherSpecialities).forEach(speciality => {
const option = document.createElement('option');
option.value = speciality.id;
option.text = speciality.code + " " + (speciality.name.includes('Программная инженерия') ? '🏆 ' + speciality.name : speciality.name);
specialitySelect.add(option);
});
let specialityData = localStorage.getItem('speciality');
if (specialityData) {
specialitySelect.value = specialityData;
}
validateForm();
});
// Загрузка данных из хранилища если данные есть
function loadFromLocalStorage() {
const phoneNumber = localStorage.getItem('phoneNumber');
@@ -71,6 +92,7 @@ export function onLoad() {
const isKursSelected = Array.from(kursElements).some(k => k.checked);
const isPhoneNumberValid = phoneNumberInput.value.length === 12;
// Блокировка курсов в зависимости от выбранного типа обучения
if (isOptionSelected) {
var typeOfEducation = (Array.from(options).find(option => option.checked).getAttribute('aria-label'));
if (typeOfEducation === "Баклан") {
@@ -100,26 +122,28 @@ export function onLoad() {
kursElement5.disabled = false;
}
}
if (isOptionSelected && isKursSelected && isPhoneNumberValid) {
if (isOptionSelected && isKursSelected && isPhoneNumberValid && specialitySelect.value !== '0') {
continueButton.classList.remove('btn-disabled');
localStorage.setItem('phoneNumber', phoneNumberInput.value);
localStorage.setItem('paid', checkboxPaid.checked);
localStorage.setItem('ochno', !checkboxOchno.checked);
localStorage.setItem('option', typeOfEducation);
localStorage.setItem('kurs', Array.from(kursElements).find(k => k.checked).getAttribute('aria-label'));
localStorage.setItem('speciality', specialitySelect.value);
} else {
continueButton.classList.add('btn-disabled');
}
}
loadFromLocalStorage();
options.forEach(option => option.addEventListener('change', validateForm));
kursElements.forEach(k => k.addEventListener('change', validateForm));
phoneNumberInput.addEventListener('input', validateForm);
checkboxPaid.addEventListener('change', validateForm);
checkboxOchno.addEventListener('change', validateForm);
specialitySelect.addEventListener('change', validateForm);
loadFromLocalStorage();
validateForm();
}

View File

@@ -1,19 +1,20 @@
@page "/otchislenie/result"
@using Microsoft.AspNetCore.Authorization
@page "/result"
@using Otchinslator.Components.Layout
@layout OtchislenieLayout
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
<PageTitle>Уже почти | Отчислятор 3000</PageTitle>
<dialog id="SendToDirectorModal" class="modal modal-bottom sm:modal-middle">
<dialog id="printModal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="text-lg font-bold">Отправка письма</h3>
<p class="py-4">Вы уверены что хотите отправить ваше заявление директору?<br>Отменить отправку невозможно!</p>
<div class="modal-action">
<h3 class="text-lg font-bold">Есть проблема...</h3>
<p class="py-4 font-bold text-center">К сожалению отправить или распечатать заявление нельзя.</p>
<img src="/img/write.png" class="h-80 my-4 mx-auto" alt=""/>
<p class="text-center">Вам нужно написать его от руки, сфотографировать и отправить замдиректора по учебной работе на почту.</p>
<div class="modal-action justify-center">
<form method="dialog">
<button class="btn btn-primary">Нет</button>
<a href="/otchislenie/congratulation" class="btn btn-error">Да</a>
<button class="btn btn-primary">Ок</button>
</form>
</div>
</div>
@@ -24,16 +25,19 @@
class="text-center font-bold text-4xl md:text-5xl w-max absolute left-1/2 -top-1/4 transform -translate-x-1/2 italic">
<br>Заявление готово
</div>
<object data="/getStatement" type="application/pdf" class="w-96 sm:w-[32rem] h-[30rem] rounded-2xl">
<p class="text-center">Не удалось отобразить заявление, попробуйте скачать <a href="/getStatement">(тык)</a></p>
</object>
<div id="pdfrenderer">
<object data="/getStatement" type="application/pdf" class="w-96 sm:w-[32rem] h-[30rem] rounded-2xl text-center">
<p>Не удалось отобразить заявление</p>
<button id="alterRender" class="btn btn-primary my-2">использовать альтернативную отрисовку</button>
<p>или откойте в браузере на основе Firefox</p>
</object>
</div>
<div class="join w-full mt-4 flex gap-2">
<a href="/" class="relative btn btn-primary rounded-full flex-grow-0 w-[3rem] h-[3rem]">
<img class="absolute p-3" src="img/exit.svg" alt=""/>
</a>
<button onclick="SendToDirectorModal.showModal()"
class="btn btn-primary rounded-full flex-grow w-30">
Отправить директору
<button onclick="printModal.showModal()" class="btn btn-primary rounded-full flex-grow w-30">
Печать
</button>
<a id="downloadPDF" target="_blank" href="/getStatement"
class="btn btn-primary bg-base-200 border-base-200 rounded-full flex-grow-0 w-[3rem]">
@@ -43,37 +47,32 @@
</div>
</a>
</div>
@* <div class="card rounded-badge bg-base-200 p-4"> *@
@* <div class="flex flex-col space-y-4 mt-1"> *@
@* <button onclick="SendToDirectorModal.showModal()" *@
@* class="btn h-16 btn-primary rounded-full text-2xl relative"> *@
@* Отправить директору *@
@* </button> *@
@* <a id="downloadPDF" target="_blank" href="/getStatement" *@
@* class="btn h-16 btn-primary rounded-full text-2xl relative"> *@
@* Скачать PDF *@
@* <div class="absolute bg-base-200 rounded-full right-1 w-14 h-14"> *@
@* <img class="p-3" src="img/pdf.svg" alt=""/> *@
@* </div> *@
@* </a> *@
@* </div> *@
@* </div> *@
@* </div> *@
@* $1$ TODO: Сделать адаптив #1# *@
@* <div class="mt-9 flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0 w-96 mx-auto" > *@
@* <div class="mt-9 flex flex-col space-y-4 w-96 mx-auto"> *@
@* <a href="/statement" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> *@
@* Назад *@
@* </a> *@
@* <a href="/" class="btn w-96 h-14 btn-primary rounded-full text-2xl"> *@
@* Выход *@
@* </a> *@
@* </div> *@
<div class="w-96 mx-auto mt-6">
<a id="congratulation" href="/otchislenie/congratulation"
class="btn w-full h-16 btn-primary rounded-full text-2xl hidden">
Страница поздравления
</a>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.10.377/pdf.min.js"></script>
<script>
const alterRenderButton = document.getElementById('alterRender');
alterRenderButton.addEventListener('click', alterRender);
function alterRender() {
const pdfrenderer = document.getElementById('pdfrenderer');
const url = '/getStatement';
pdfrenderer.innerHTML = '<canvas id="pdf-canvas" class="w-96 rounded-2xl"></canvas>';
pdfjsLib.getDocument(url).promise.then(function (pdf) {
pdf.getPage(1).then(function (page) {
const viewport = page.getViewport({scale: 1});
const canvas = document.getElementById('pdf-canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
const renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext);
});
});
}
</script>

View File

@@ -27,13 +27,17 @@ export function onLoad() {
console.log('Начата генерация заявления');
generateStatementModal.showModal();
console.log(localStorage.getItem('ochno'))
const data = {
"phone": "+7 " + localStorage.getItem('phoneNumber'),
"kurs": localStorage.getItem('kurs'),
"isFreeEducation": localStorage.getItem('paid') === "false",
"isOchno": localStorage.getItem('ochno') === true,
"speciality": optionMapping[localStorage.getItem('option')],
"reason": localStorage.getItem('statement')
"isOchno": localStorage.getItem('ochno') === "true",
"typeOfEducation": optionMapping[localStorage.getItem('option')],
"reason": localStorage.getItem('statement'),
"institut": localStorage.getItem('institut'),
"direction": localStorage.getItem('speciality')
}
try {
@@ -47,7 +51,7 @@ export function onLoad() {
if (response.ok) {
console.log('Заявление успешно сгенерировано');
location.href='otchislenie/result';
location.href='/result';
} else {
hideLoadingModal();
console.log('Ошибка при генерации заявления');

View File

@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Mvc;
namespace Otchinslator.Controllers;
[ApiController]
public class PublicDataController(DatabaseContext db) : Controller
{
[HttpGet("/api/getSpecialities")]
public IActionResult GetSpecialities(int institutId)
{
if (institutId <= 0 || institutId > 4)
{
return BadRequest("Некорректный институт\nДоступные институты: 1 - ИКТИБ, 2 - ИРТСУ, 3 - ИУЭС, 4 - ИНЭП");
}
var specialities = db.Specialties.Where(s => s.InstitutId == institutId).ToList();
var specialitiesDto = specialities.Select(s => new
{
s.Id,
s.Name,
s.Code,
});
return Ok(specialitiesDto);
}
}

View File

@@ -7,17 +7,17 @@ namespace Otchinslator;
[ApiController]
[Authorize]
public class StatementController(IStatementGenerator statementGenerator, IConfiguration configuration) : Controller
public class StatementController(IStatementGenerator statementGenerator, IConfiguration configuration, DatabaseContext db) : Controller
{
[HttpPost("/generateStatement")]
public async Task<IActionResult> GenerateStatement(UserDTO userDto)
{
var speciality = userDto.speciality switch
var speciality = userDto.typeOfEducation switch
{
1 => SpecialityType.Bakalavriat,
2 => SpecialityType.Magistatura,
3 => SpecialityType.Specialitet,
_ => throw new ArgumentOutOfRangeException()
_ => throw new ArgumentOutOfRangeException() // Модель валидации не позволит передать некорректное значение
};
if (speciality == SpecialityType.Bakalavriat && userDto.kurs > 4)
@@ -45,6 +45,13 @@ public class StatementController(IStatementGenerator statementGenerator, IConfig
userFIO = petrovich.InflectFirstNameTo(Case.Genitive) + " " + petrovich.InflectLastNameTo(Case.Genitive) + " " + petrovich.InflectMiddleNameTo(Case.Genitive);
var direction = db.Specialties.FirstOrDefault(x => x.Id == userDto.direction);
if (direction == null)
{
Utils.LogToTg("Походу ломают `direction == null`", configuration);
return BadRequest("Некорректное направление");
}
var userData = new UserData
{
reason = userDto.reason,
@@ -54,7 +61,8 @@ public class StatementController(IStatementGenerator statementGenerator, IConfig
kurs = userDto.kurs,
isFreeEducation = userDto.isFreeEducation,
isOchno = userDto.isOchno,
speciality = speciality
speciality = speciality,
direction = direction.Code + " " + direction.Name.ToLower()
};
var statement = await statementGenerator.GenerateStatementAsync(userData);
var pdf = await statementGenerator.ConvertToPDFAsync(statement);
@@ -79,12 +87,5 @@ public class StatementController(IStatementGenerator statementGenerator, IConfig
outStream.Position = 0;
return File(outStream, "application/pdf");
}
// [HttpGet("/getUserData")]
// public async Task<IActionResult> getUserdata()
// {
// var userFIO = User.Claims.FirstOrDefault(x => x.Type == "name")?.Value;
// var userdata = User.Identities.FirstOrDefault()?.Claims.Select(x => new {x.Type, x.Value});
// return Ok(userdata);
// }
}

View File

@@ -20,6 +20,11 @@ public class UserDTO
public bool isOchno { get; set; }
[Required]
[Range(1, 3, ErrorMessage = "Некорректный тип специальности")]
public int speciality { get; set; }
public int typeOfEducation { get; set; }
[Required]
[Range(1, 4, ErrorMessage = "Некорректный институт")]
public int institut { get; set; }
[Required]
public int direction { get; set; }
}

View File

@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore;
using Otchinslator.Tables;
namespace Otchinslator;
public class DatabaseContext : DbContext
{
public DbSet<Institut> Instituts { get; set; }
public DbSet<Specialty> Specialties { get; set; }
public string DbPath { get; }
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Institut>().HasKey(i => i.Id);
modelBuilder.Entity<Specialty>().HasKey(s => s.Id);
modelBuilder.Entity<Institut>().HasData(
new Institut() { Id = 1, ShortName = "ИКТИБ", Name = "Институт компьютерных технологий и информационной безопасности" },
new Institut() { Id = 2, ShortName = "ИРТСУ", Name = "Институт радиотехнических систем и управления" },
new Institut() { Id = 3, ShortName = "ИУЭС", Name = "Институт управления в экономических, экологических и социальных системах" },
new Institut() { Id = 4, ShortName = "ИНЭП", Name = "Институт нанотехнологий, электроники и приборостроения" }
);
modelBuilder.Entity<Specialty>().HasData(
// ИКТИБ
// Бакалавриат
new Specialty() { Id = 1, Name = "Математическое обеспечение и администрирование информационных систем", Code = "02.03.03", InstitutId = 1 },
new Specialty() { Id = 2, Name = "Информатика и вычислительная техника", Code = "09.03.01", InstitutId = 1 }, // + заочка
new Specialty() { Id = 3, Name = "Информационные системы и технологии", Code = "09.03.02", InstitutId = 1 },
new Specialty() { Id = 4, Name = "Программная инженерия", Code = "09.03.04", InstitutId = 1 },
new Specialty() { Id = 5, Name = "Информационная безопасность", Code = "10.03.01", InstitutId = 1 },
new Specialty() { Id = 6, Name = "Системный анализ и управление", Code = "27.03.03", InstitutId = 1 },
// Специалитет
new Specialty() { Id = 7, Name = "Применение и эксплуатация автоматизированных систем специального назначения", Code = "09.05.01", InstitutId = 1 },
new Specialty() { Id = 8, Name = "Информационная безопасность телекоммуникационных систем", Code = "10.05.02", InstitutId = 1 },
new Specialty() { Id = 9, Name = "Информационная безопасность автоматизированных систем", Code = "10.05.03", InstitutId = 1 },
new Specialty() { Id = 10, Name = "Безопасность информационных технологий в правоохранительной сфере", Code = "10.05.05", InstitutId = 1 },
// ИРТСУ
new Specialty() { Id = 11, Name = "НЕ ЗАПОЛНЕНО", Code = "0.0.0", InstitutId = 2 },
// ИУЭС
new Specialty() { Id = 12, Name = "НЕ ЗАПОЛНЕНО", Code = "0.0.0", InstitutId = 3 },
// ИНЭП
new Specialty() { Id = 13, Name = "НЕ ЗАПОЛНЕНО", Code = "0.0.0", InstitutId = 4 }
);
}
}

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0.1-alpine3.21 AS base
FROM mcr.microsoft.com/dotnet/aspnet:9.0.2-alpine3.21 AS base
USER root
RUN apk update && apk add --no-cache curl icu tzdata musl-locales musl-locales-lang
USER $APP_UID

View File

@@ -28,17 +28,15 @@
<PackageReference Include="BlazorJSComponents" Version="1.0.0" />
<PackageReference Include="BlazorPageScript"
Version="1.0.0" />
<PackageReference Include="DocumentFormat.OpenXml"
Version="3.2.0" />
<PackageReference Include="Gotenberg.Sharp.API.Client"
Version="2.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
<PackageReference Include="Gotenberg.Sharp.API.Client" Version="2.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.6.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="3.6.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.8.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="3.8.0" />
<PackageReference Include="NPetrovich"
Version="2.0.1" />
</ItemGroup>

View File

@@ -4,8 +4,10 @@ using Gotenberg.Sharp.API.Client.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using Otchinslator;
using Otchinslator.Components;
using Otchinslator.Services;
@@ -22,16 +24,22 @@ builder.Services.AddControllersWithViews(options =>
}).AddMicrosoftIdentityUI().AddDataAnnotationsLocalization();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
var folder = Path.Join(baseDirectory, "data");
if (!Directory.Exists(folder))
Directory.CreateDirectory(folder);
folder = Path.Join(folder, "database.db");
builder.Services.AddDbContext<DatabaseContext>(options => options.UseSqlite($"Data Source={folder}"));
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddJSComponents();
builder.Services.AddRazorPages(); //////////////
builder.Services.AddServerSideBlazor(); //////////////
builder.Services.AddOptions<GotenbergSharpClientOptions>()
@@ -54,6 +62,6 @@ app.UseAuthorization();
app.UseForwardedHeaders();
app.MapRazorComponents<App>();
// .AddInteractiveServerRenderMode();
// .AddInteractiveServerRenderMode();
app.Run();
app.Run();

View File

@@ -8,7 +8,7 @@ namespace Otchinslator.Services;
public interface IStatementGenerator
{
public Task<MemoryStream> GenerateStatementAsync(UserData userData);
public Task<MemoryStream> GenerateStatementAsync(UserData userData, string TemplateName = "ictis.docx");
public Task<Stream> ConvertToPDFAsync(MemoryStream stream);
}
@@ -18,12 +18,13 @@ public class StatementGenerator(GotenbergSharpClient gotenbergSharpClient) : ISt
private const string SpecialitetText = "специалитета по специальности";
private const string BakalavriatText = "бакалавриата по направлению подготовки";
private const string MagistaturaText = "магистратуры по направлению подготовки";
private const string FreeEducationText = "обучение за счет ассигнований федерального бюджета";
private const string FreeEducationText = "за счет ассигнований федерального бюджета";
private const string PaidEducationText = "на договорной (платной) основе";
public async Task<MemoryStream> GenerateStatementAsync(UserData userData)
// TODO: Выбор темплейтов не реализован
public async Task<MemoryStream> GenerateStatementAsync(UserData userData, string TemplateName = "ictis.docx")
{
byte[] textByteArray = File.ReadAllBytes("Templates/ictis.docx");
byte[] textByteArray = File.ReadAllBytes("Templates/" + TemplateName);
MemoryStream stream = new MemoryStream();
stream.Write(textByteArray, 0, textByteArray.Length);
using (WordprocessingDocument doc = WordprocessingDocument.Open(stream, true))
@@ -68,7 +69,7 @@ public class StatementGenerator(GotenbergSharpClient gotenbergSharpClient) : ISt
SpecialityType.Magistatura => MagistaturaText,
SpecialityType.Specialitet => SpecialitetText,
_ => throw new ArgumentOutOfRangeException()
} + " ничего не делания";
} + " " + userData.direction;
break;
}

View File

@@ -0,0 +1,10 @@
namespace Otchinslator.Tables;
public class Institut
{
public int Id { get; set; }
public string Name { get; set; }
public string ShortName { get; set; }
public List<Specialty> Specialties { get; } = new();
}

View File

@@ -0,0 +1,10 @@
namespace Otchinslator.Tables;
public class Specialty
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public int InstitutId { get; set; }
public Institut Institut { get; set; }
}

Binary file not shown.

View File

@@ -12,7 +12,8 @@ public class UserData
int kurs,
bool isFreeEducation,
bool isOchno,
SpecialityType speciality)
SpecialityType speciality,
string direction)
{
}
@@ -28,6 +29,7 @@ public class UserData
public bool isFreeEducation { get; set; }
public bool isOchno { get; set; }
public SpecialityType speciality { get; set; }
public string direction { get; set; }
}
public enum SpecialityType

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@@ -1,6 +0,0 @@
services:
otchinslator:
image: otchinslator
build:
context: .
dockerfile: Otchinslator/Dockerfile